From 2f774ba9cebb8ca3b90d3156f9abb75761b0c96f Mon Sep 17 00:00:00 2001 From: Randy Syring Date: Fri, 1 Aug 2025 18:21:57 -0400 Subject: [PATCH 01/33] JavaScript: remove date time polyfill All modern browsers support datetime-local --- webgrid/static/webgrid.js | 185 -------------------------------------- 1 file changed, 185 deletions(-) diff --git a/webgrid/static/webgrid.js b/webgrid/static/webgrid.js index 9a47625..c7263c9 100644 --- a/webgrid/static/webgrid.js +++ b/webgrid/static/webgrid.js @@ -228,8 +228,6 @@ function datagrid_toggle_filter_inputs(jq_filter_tr) { fields2.hide(); fields2.val(''); } - - wgDatetimePolyfill.replaceInputs.bind(wgDatetimePolyfill)(); } } @@ -316,186 +314,3 @@ function datagrid_cleanup_before_form_submission() { }); return true; } - - -/* Many browsers support datetime-local input type, but Firefox currently does not. The -following polyfill has some minimal edits to go with Webgrid usage, and provides Firefox -with a similar UX to other browsers. The datetime-local is shown as two inputs of -supported types, and input is combined into a hidden field for submission as a single -value. */ - -/** - * datetime-polyfill - * @version 1.0.0 - * @author Andchir - */ - - (function (factory) { - - if ( typeof define === 'function' && define.amd ) { - - // AMD. Register as an anonymous module. - define([], factory); - - } else if ( typeof exports === 'object' ) { - - // Node/CommonJS - module.exports = factory(); - - } else { - - // Browser globals - window.WGDatetimePolyfill = factory(); - } - -}(function( ) { - - 'use strict'; - - function WGDatetimePolyfill(initOptions) { - const defaultOptions = {force: false}; - const options = { - ...defaultOptions, - ...(initOptions || {}), - }; - - const self = this; - - this.init = function(force) { - if (force) { - this.replaceInputs.bind(this)(); - } else { - this.onReady(this.replaceInputs.bind(this)); - } - }; - - this.onReady = function(cb) { - if (document.readyState !== 'loading') { - cb(); - } else { - document.addEventListener('DOMContentLoaded', cb); - } - }; - - this.replaceInputs = function() { - const replacedInputs = []; - const inputs = document.querySelectorAll( - '.datagrid input[type="datetime"], .datagrid input[type="datetime-local"]' - ); - - const onChangeFunc = function(input, inpDate, inpTime) { - const valueDate = inpDate.value; - const valueTime = inpTime.value; - if (!valueDate || !valueTime) { - input.value = ''; - return; - } - input.value = valueDate + 'T' + valueTime; - }; - - Array.from(inputs) - .filter(function (item,index) { return item.style.display!="none" } ) - .forEach(function(input) { - if (['datetime', 'datetime-local'].indexOf(input.type) > -1) { - return; - } - input.type = 'hidden'; - const values = self.parseValue(input.value); - const inpDate = self.createInput('date', input.className, { - width: '55%', - boxSizing: 'border-box', - display: 'block', - float: 'left', - borderWidth: '1px', - borderRight: 0, - borderTopRightRadius: 0, - borderBottomRightRadius: 0, - marginTop: '3px' - }, function() { - onChangeFunc(input, inpDate, inpTime); - }); - inpDate.setAttribute('name', 'polyfill_' + input.name); - inpDate.setAttribute('form', 'webgrid-fake'); - if (values.length === 2) { - inpDate.value = values[0]; - } - - const inpTime = self.createInput('time', input.className, { - width: '45%', - boxSizing: 'border-box', - display: 'block', - float: 'left', - borderWidth: '1px', - borderLeft: 0, - borderTopLeftRadius: 0, - borderBottomLeftRadius: 0, - marginTop: '3px' - }, function() { - onChangeFunc(input, inpDate, inpTime); - }); - inpTime.setAttribute('name', 'polyfill_' + input.name); - inpTime.setAttribute('form', 'webgrid-fake'); - if (values.length === 2) { - inpTime.value = values[1]; - } - - const divEl = document.createElement('div'); - divEl.style.clear = 'left'; - - if(input.nextSibling){ - input.parentNode.insertBefore(inpDate, input.nextSibling); - input.parentNode.insertBefore(inpTime, input.nextSibling); - input.parentNode.insertBefore(divEl, input.nextSibling); - }else{ - input.parentNode.appendChild(inpDate); - input.parentNode.appendChild(inpTime); - input.parentNode.appendChild(divEl); - } - - replacedInputs.push(input); - }); - - return replacedInputs; - }; - - this.createInput = function(type, className, styles, onChange) { - const inp = document.createElement('input'); - inp.type = type; - inp.className = className; - if (styles) { - this.css(inp, styles); - } - if (typeof onChange === 'function') { - inp.onchange = onChange.bind(inp); - } - return inp; - }; - - this.parseValue = function(value) { - return value && /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(value) - ? value.split('T') - : []; - }; - - this.css = function (el, styles) { - this.forEachObj(styles, function (key, val) { - el.style[key] = val; - }); - }; - - this.forEachObj = function (obj, callback) { - for (let prop in obj) { - if (obj.hasOwnProperty(prop)) { - callback(prop, obj[prop]); - } - } - return obj; - }; - - this.init(options.force); - } - - return WGDatetimePolyfill; -})); - -const wgDatetimePolyfill = new WGDatetimePolyfill(); \ No newline at end of file From d413b25d84ae7a4b97822fdc76ef47375115316c Mon Sep 17 00:00:00 2001 From: Randy Syring Date: Fri, 1 Aug 2025 19:23:27 -0400 Subject: [PATCH 02/33] Fix imports when openpyxl isn't present Fixes #189 --- webgrid/renderers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webgrid/renderers.py b/webgrid/renderers.py index 320523b..8b6f189 100644 --- a/webgrid/renderers.py +++ b/webgrid/renderers.py @@ -9,6 +9,7 @@ from collections import defaultdict import json from typing import Dict, Union +import typing import six from blazeutils.functional import identity @@ -38,7 +39,7 @@ from openpyxl.styles import Font, Border, Side, Alignment from openpyxl.utils import get_column_letter else: - Font = Border = Side = Alignment = None + Font = Border = Side = Alignment = typing.Any try: from morphi.helpers.jinja import configure_jinja_environment From 4a3d9f30a8728cee4357115a599dc9c973a6527e Mon Sep 17 00:00:00 2001 From: Randy Syring Date: Fri, 1 Aug 2025 19:27:29 -0400 Subject: [PATCH 03/33] Rendering: fix paging_select() input display fixes #190 --- webgrid/renderers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/webgrid/renderers.py b/webgrid/renderers.py index 8b6f189..ce5597e 100644 --- a/webgrid/renderers.py +++ b/webgrid/renderers.py @@ -753,8 +753,10 @@ def paging_select(self): op_qsk = self.grid.prefix_qs_arg_key('onpage') return self._render_jinja( ''' - {{text}} + + {{text}} + ''', name=op_qsk, page_count=self.grid.page_count, From ff982a2cce1381060a2c79de72c793747418db4b Mon Sep 17 00:00:00 2001 From: Randy Syring Date: Sat, 2 Aug 2025 11:23:43 -0400 Subject: [PATCH 04/33] Dev tooling / setup --- mise.toml | 29 ++++++++++++++++++ pyproject.toml | 48 +++++++++++++++++++++++++++++ setup.py | 83 -------------------------------------------------- tox.ini | 13 ++++---- 4 files changed, 84 insertions(+), 89 deletions(-) create mode 100644 mise.toml create mode 100644 pyproject.toml delete mode 100644 setup.py diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..769a432 --- /dev/null +++ b/mise.toml @@ -0,0 +1,29 @@ +[env] +PROJECT_SLUG = '{{ config_root | basename | slugify }}' + +_.python.venv.path = '{% if env.UV_PROJECT_ENVIRONMENT %}{{ env.UV_PROJECT_ENVIRONMENT }}{% else %}.venv{% endif %}' +_.python.venv.create = true + +[tools] +python = ['3.10', '3.11', '3.12', '3.13'] + + +[task_config] +includes = [ + 'tasks', +] + + +################ TASKS ################# +[tasks.pytest-cov] +description = 'Full pytest run with html coverage report' +# .coveragerc sets directory to ./tmp/coverage-html +run = 'pytest --cov --cov-report=html --no-cov-on-fail' + + +[tasks.upgrade-deps] +description = 'Upgrade uv and pre-commit dependencies' +run = [ + 'uv sync --upgrade', + 'pre-commit autoupdate', +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ddfb30b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,48 @@ +[build-system] +requires = [ + "setuptools>=61.0", + "wheel", + "Babel", +] +build-backend = "setuptools.build_meta" + +[project] +name = "WebGrid" +# version is read from webgrid/version.py at build time +dynamic = ["version"] +description = "A library for rendering HTML tables and Excel files from SQLAlchemy models." +readme = { file = "readme.rst", content-type = "text/x-rst" } +license = { text = "BSD-3-Clause" } +authors = [ + { name = "Randy Syring", email = "randy.syring@level12.io" }, +] +urls = { "homepage" = "https://github.com/level12/webgrid" } +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", +] +dependencies = [ + "BlazeUtils>=0.6.0", + "SQLAlchemy>=1.4.20", + "jinja2", + "python-dateutil", + "Werkzeug", +] + +[project.optional-dependencies] +i18n = ["morphi"] + +[project.scripts] +webgrid_ta = "webgrid_ta.manage:script_entry" + +[tool.setuptools.dynamic] +version = { attr = "webgrid.version:VERSION" } + +[tool.setuptools.packages.find] +include = ["webgrid"] + +[tool.setuptools] +# honors your MANIFEST.in / include_package_data=True +include-package-data = true diff --git a/setup.py b/setup.py deleted file mode 100644 index 4480789..0000000 --- a/setup.py +++ /dev/null @@ -1,83 +0,0 @@ -import os.path as osp -try: - from setuptools import setup -except ImportError: - from ez_setup import use_setuptools - use_setuptools() - from setuptools import setup - - -# pip install -e .[develop] -develop_requires = [ - 'arrow', - 'coverage', - 'mock', - 'openpyxl', - 'psycopg2-binary', - 'pyodbc', - 'pytest', - 'pytest-cov', - 'Flask', - 'Flask-Bootstrap', - 'Flask-SQLAlchemy', - 'Flask-WebTest', - 'Flask-WTF', - 'pyquery', - 'sqlalchemy_pyodbc_mssql', - 'sqlalchemy_utils', - 'wrapt', - 'xlsxwriter', -] - -cdir = osp.abspath(osp.dirname(__file__)) -README = open(osp.join(cdir, 'readme.rst')).read() -CHANGELOG = open(osp.join(cdir, 'changelog.rst')).read() - -version_fpath = osp.join(cdir, 'webgrid', 'version.py') -version_globals = {} -with open(version_fpath) as fo: - exec(fo.read(), version_globals) - -setup( - name="WebGrid", - version=version_globals['VERSION'], - description="A library for rendering HTML tables and Excel files from SQLAlchemy models.", - long_description='\n\n'.join((README, CHANGELOG)), - author="Randy Syring", - author_email="randy.syring@level12.io", - url='https://github.com/level12/webgrid', - classifiers=[ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - ], - license='BSD-3-Clause', - packages=['webgrid'], - extras_require={ - 'develop': develop_requires, - 'i18n': [ - 'morphi' - ] - }, - zip_safe=False, - include_package_data=True, - setup_requires=[ - 'Babel' - ], - install_requires=[ - 'BlazeUtils>=0.6.0', - 'dataclasses; python_version < "3.7"', - 'SQLAlchemy>=1.4.20', - 'jinja2', - 'python-dateutil', - 'Werkzeug', - ], - entry_points=""" - [console_scripts] - webgrid_ta = webgrid_ta.manage:script_entry - """, -) diff --git a/tox.ini b/tox.ini index 5ffc1b2..787356c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{310,311}-{base,i18n,stable},py312-{base,stable},flake8,docs,i18n +envlist = py{310,311,312,313}-{base,i18n,stable},flake8,docs [testenv] @@ -14,10 +14,10 @@ recreate = true passenv = SQLALCHEMY_DATABASE_URI commands = - stable: pip install --progress-bar off -r ./stable-requirements.txt - base,stable: pip install --progress-bar off -e .[develop] - i18n: pip install --progress-bar off -e .[develop,i18n] - pip freeze + stable: uv pip install -r ./stable-requirements.txt + base,stable: uv pip install -e .[develop] + i18n: uv pip install -e .[develop,i18n] + uv pip freeze py.test \ -ra \ --tb native \ @@ -36,13 +36,14 @@ commands = python webgrid_ta/manage.py verify-translations [testenv:flake8] +basepython = python3.12 skip_install = true usedevelop = false deps = flake8 commands = flake8 webgrid webgrid_ta webgrid_blazeweb_ta [testenv:docs] -basepython = python3.10 +basepython = python3.12 recreate = false skip_install = true usedevelop = true From 93b526507f97253c126b155b610368c348517601 Mon Sep 17 00:00:00 2001 From: Randy Syring Date: Sat, 2 Aug 2025 14:06:39 -0400 Subject: [PATCH 05/33] Apply Coppy template --- .copier-answers-py.yaml | 19 ++++++++ .coveragerc | 12 ++++- .editorconfig | 38 +++++++++++++++ .github/workflows/nox.yaml | 36 ++++++++++++++ .gitignore | 47 +++++++++++++----- .pre-commit-config.yaml | 34 +++++++++++++ ci/.gitignore | 2 + env-config.yaml | 4 ++ hatch.toml | 18 +++++++ pyproject.toml | 70 ++++++++++++++++++-------- readme.md | 50 +++++++++++++++++++ ruff.toml | 92 +++++++++++++++++++++++++++++++++++ src/conftest.py | 50 +++++++++++++++++++ src/webgrid/__init__.py | 0 src/webgrid/cli.py | 2 + src/webgrid/version.py | 1 + src/webgrid_tasks_lib.py | 29 +++++++++++ src/webgrid_tests/test_cli.py | 5 ++ tasks/bump | 80 ++++++++++++++++++++++++++++++ 19 files changed, 557 insertions(+), 32 deletions(-) create mode 100644 .copier-answers-py.yaml create mode 100644 .editorconfig create mode 100644 .github/workflows/nox.yaml create mode 100644 .pre-commit-config.yaml create mode 100644 ci/.gitignore create mode 100644 env-config.yaml create mode 100644 hatch.toml create mode 100644 readme.md create mode 100644 ruff.toml create mode 100644 src/conftest.py create mode 100644 src/webgrid/__init__.py create mode 100644 src/webgrid/cli.py create mode 100644 src/webgrid/version.py create mode 100644 src/webgrid_tasks_lib.py create mode 100644 src/webgrid_tests/test_cli.py create mode 100755 tasks/bump diff --git a/.copier-answers-py.yaml b/.copier-answers-py.yaml new file mode 100644 index 0000000..f31bd36 --- /dev/null +++ b/.copier-answers-py.yaml @@ -0,0 +1,19 @@ +# Changes here will be overwritten by Copier +# +# Updating `_src` could be helpful, see notes at: +# https://github.com/level12/coppy/wiki#creating-a-project +# +# Otherwise, NEVER EDIT MANUALLY +_commit: v1.20250622.1 +_src_path: gh:level12/coppy +author_email: devteam@level12.io +author_name: Level 12 +gh_org: level12 +gh_repo: webgrid +hatch_version_tag_sign: true +project_name: WebGrid +py_module: webgrid +python_version: '3.10' +script_name: '' +use_circleci: false +use_gh_nox: true diff --git a/.coveragerc b/.coveragerc index 5144542..27e6bf4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,3 +1,13 @@ +# .coveragerc to control coverage.py [run] branch = True -source = webgrid +omit = + src/webgrid/version.py + +source = + src/webgrid + src/webgrid_tests + + +[html] +directory = tmp/coverage-html diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..fbd51ff --- /dev/null +++ b/.editorconfig @@ -0,0 +1,38 @@ +# This is the "top-most" .editorconfig for this project. +# Like a `.gitignore` file, you can place additional +# .editorconfig files in specific directories, if you need +# local settings. However, this directive specifies that +# no "global" settings will be used (settings from higher- +# up in the directory hierarchy, wherever that may be) +root = true + +# Set the default whitespace settings for all files +[*] + +# Use UNIX-style line endings +end_of_line = lf + +# 4-space indents +indent_size = 4 +indent_style = space + +# end all files with a newline +insert_final_newline = true + +# trim whitespace from the ends of lines +trim_trailing_whitespace = true + + +[*.py] +# ensure Python source files are utf-8 +charset = utf-8 + + +[*.{yml,yaml}] +# Set two-space indents for YAML files +indent_size = 2 + + +[Makefile] +# Makefiles *must* use tabs! +indent_style = tab diff --git a/.github/workflows/nox.yaml b/.github/workflows/nox.yaml new file mode 100644 index 0000000..fed1ad1 --- /dev/null +++ b/.github/workflows/nox.yaml @@ -0,0 +1,36 @@ +name: Nox + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +# Limit this workflow to a single run at a time per-branch to avoid wasting worker resources +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + nox: + runs-on: ubuntu-latest + + # Runs all steps inside this container + container: + image: ghcr.io/level12/ubuntu-mive:24-3.12 + options: --user root + + env: + UV_LINK_MODE: copy + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Mark repo as safe for Git + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: Run Tests + run: | + uv run --only-group nox -- nox diff --git a/.gitignore b/.gitignore index dfd176e..c5d904d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,39 @@ -*.komodo* +# Python *.pyc *.egg-info -dist/* -build/* -.tox/ -venv -pytest-reports +.venv + +# Tox/Nox/Tests/Coverage +/.nox +/.tox +/ci/coverage-html *.coverage -coverage.xml -*.nose.xml -*.py~ -.eggs -.ci/* +/coverage.xml +*.pytests.xml +.pytest_cache + + +# Editor Clutter +.vscode +.idea + + +# Build related +dist +/build +npm-debug.log +.*-cache +node_modules + + +# Local dev files +/mise.local.toml +/tmp + +# Terraform +*.tfstate* +tf.plan +.terraform + + +# Project specific diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..fcbbee5 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,34 @@ +--- +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-merge-conflict + - id: check-ast + types: [ python ] + - id: debug-statements + types: [ python ] + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-added-large-files + - id: check-yaml + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.12.0 + hooks: + # i.e. `ruff check` + - id: ruff + # i.e. `ruff format --check` + - id: ruff-format + # Due to the Ruff config we use (see comment in pyproject.yaml), it's possible that the + # formatter creates linting failures. By only doing a check here, it forces the dev to run + # the formatter before pre-commit runs (presumably on save through their editor) and then + # the linting check above would always catch a problem created by the formatter. + args: [ --check ] + - repo: https://github.com/level12/pre-commit-hooks + rev: v0.20250226.1 + hooks: + - id: check-ruff-versions + - repo: https://github.com/astral-sh/uv-pre-commit + rev: 0.7.13 + hooks: + - id: uv-lock diff --git a/ci/.gitignore b/ci/.gitignore new file mode 100644 index 0000000..5c8ab2a --- /dev/null +++ b/ci/.gitignore @@ -0,0 +1,2 @@ +artifacts +test-reports diff --git a/env-config.yaml b/env-config.yaml new file mode 100644 index 0000000..2567aa4 --- /dev/null +++ b/env-config.yaml @@ -0,0 +1,4 @@ +profile: + pypi: + HATCH_INDEX_USER: '__token__' + HATCH_INDEX_AUTH: 'op://private/pypi.python.org/api-token' diff --git a/hatch.toml b/hatch.toml new file mode 100644 index 0000000..53b3068 --- /dev/null +++ b/hatch.toml @@ -0,0 +1,18 @@ +## Build +[build] +directory = 'tmp/dist' +dev-mode-dirs = ['src'] + +[build.targets.wheel] +packages = ['src/webgrid'] + + +## Env: default +[envs.default] +installer = "uv" + + +## Version +[version] +source = 'regex_commit' +path = 'src/webgrid/version.py' diff --git a/pyproject.toml b/pyproject.toml index ddfb30b..76ce608 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,21 +1,21 @@ [build-system] requires = [ - "setuptools>=61.0", - "wheel", - "Babel", + 'hatchling', + 'hatch-regex-commit', ] -build-backend = "setuptools.build_meta" +build-backend = 'hatchling.build' + [project] -name = "WebGrid" -# version is read from webgrid/version.py at build time -dynamic = ["version"] +name = 'WebGrid' description = "A library for rendering HTML tables and Excel files from SQLAlchemy models." -readme = { file = "readme.rst", content-type = "text/x-rst" } -license = { text = "BSD-3-Clause" } authors = [ - { name = "Randy Syring", email = "randy.syring@level12.io" }, + {name = 'Level 12', email = 'devteam@level12.io'}, ] +requires-python = '~=3.10.0' +dynamic = ['version'] +readme = { file = "readme.rst", content-type = "text/x-rst" } +license = { text = "BSD-3-Clause" } urls = { "homepage" = "https://github.com/level12/webgrid" } classifiers = [ "Development Status :: 4 - Beta", @@ -23,6 +23,8 @@ classifiers = [ "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", ] + +# Dependencies dependencies = [ "BlazeUtils>=0.6.0", "SQLAlchemy>=1.4.20", @@ -34,15 +36,43 @@ dependencies = [ [project.optional-dependencies] i18n = ["morphi"] -[project.scripts] -webgrid_ta = "webgrid_ta.manage:script_entry" +[dependency-groups] +# Note: keeping Coppy deps grouped separate from app deps should help avoid unnecessary +# conflicts when upgrading to the latest Coppy template. +dev = [ + # From Coppy: + {include-group = "tests"}, + {include-group = "pre-commit"}, + {include-group = "audit"}, + {include-group = "nox"}, + 'click', + 'hatch', + 'ruff', -[tool.setuptools.dynamic] -version = { attr = "webgrid.version:VERSION" } - -[tool.setuptools.packages.find] -include = ["webgrid"] + # App specific: + # TODO: fill in app deps here +] +# Used by nox +tests = [ + # From Coppy: + 'pytest', + 'pytest-cov', -[tool.setuptools] -# honors your MANIFEST.in / include_package_data=True -include-package-data = true + # App specific: + # TODO: fill in app deps here +] +# Used by nox +pre-commit = [ + # From Coppy: + 'pre-commit', + 'pre-commit-uv', +] +# Used by nox +audit = [ + # From Coppy: + 'pip-audit', +] +# Used by CI +nox = [ + 'nox', +] diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..eb92108 --- /dev/null +++ b/readme.md @@ -0,0 +1,50 @@ +# WebGrid +[![nox](https://github.com/level12/webgrid/actions/workflows/nox.yaml/badge.svg)](https://github.com/level12/webgrid/actions/workflows/nox.yaml) + +## Dev + +### Copier Template + +Project structure and tooling mostly derives from the [Coppy](https://github.com/level12/coppy), +see its documentation for context and additional instructions. + +This project can be updated from the upstream repo, see +[Updating a Project](https://github.com/level12/coppy?tab=readme-ov-file#updating-a-project). + +### Project Setup + +From zero to hero (passing tests that is): + +1. Ensure [host dependencies](https://github.com/level12/coppy/wiki/Mise) are installed + +2. Start docker service dependencies (if applicable): + + `docker compose up -d` + +3. Sync [project](https://docs.astral.sh/uv/concepts/projects/) virtualenv w/ lock file: + + `uv sync` + +4. Configure pre-commit: + + `pre-commit install` + +5. Run tests: + + `nox` + +### Versions + +Versions are date based. A `bump` action exists to help manage versions: + +```shell + + # Show current version + mise bump --show + + # Bump version based on date, tag, and push: + mise bump + + # See other options + mise bump -- --help +``` diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..a08ea7c --- /dev/null +++ b/ruff.toml @@ -0,0 +1,92 @@ +line-length = 100 +target-version = 'py310' +output-format = 'concise' + +[format] +line-ending = 'lf' +quote-style = 'single' + + +[lint] +fixable = [ + 'C4', + 'COM', + 'I', + 'ISC', + 'PIE', + 'Q', + 'UP', + + 'E711', # Comparison to `None` should be `cond is None` + 'E712', # Comparison to `True` should be `cond is True` or `if cond:` + 'E713', # Test for membership should be `not in` + 'E714', # Test for object identity should be `is not` + 'F901', # `raise NotImplemented` should be `raise NotImplementedError` + 'RUF100', # Unused blanket noqa directive + 'W291', # Trailing whitespace + 'W293', # Blank line contains whitespace +] +select = [ + 'E', # ruff default: pycodestyle errors + 'W', # pycodestyle warnings + 'F', # ruff default: pyflakes + 'I', # isort + 'Q', # flake8-quotes + 'UP', # pyupgrade + 'YTT', # flake8-2020 + 'B', # flake8-bandit + 'A', # flake8-builtins + 'C4', # flake8-comprehensions + 'T10', # flake8-debugger + 'DJ', # flake8-django + 'EXE', # flake8-executable + 'PIE', # flake8-pie + 'COM', # flake-8 commas + 'RUF', # ruff specific + 'SIM', # flake8-simplify + 'ISC', # https://pypi.org/project/flake8-implicit-str-concat/ + 'PTH', # flake8-use-pathlib + # 'DTZ', # flake8-datetimez + +] +ignore = [ + 'A003', # Class attribute is shadowing a Python builtin + 'E731', # Do not assign a `lambda` expression, use a `def` + + # Rules that conflict with the formatter. See: + # + # * https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules + # * https://github.com/level12/coppy/issues/13 + # + # These are all redundant when the formatter is being used + 'W191', # tab-indentation + 'E111', # indentation-with-invalid-multiple + 'E114', # indentation-with-invalid-multiple-comment + 'E117', # over-indented + 'D206', # indent-with-spaces + 'D300', # triple-single-quotes + 'Q000', # bad-quotes-inline-string + 'Q001', # bad-quotes-multiline-string + 'Q002', # bad-quotes-docstring + 'Q003', # avoidable-escaped-quote +] + + +[lint.per-file-ignores] +'tasks/*.py' = ['EXE003'] + + +[lint.flake8-builtins] +ignorelist = ['id', 'help', 'compile'] + + +[lint.flake8-quotes] +# Prefer using different quote to escaping strings +avoid-escape = true +inline-quotes = 'single' + + +[lint.isort] +lines-after-imports = 2 +force-sort-within-sections = true +known-first-party = ['webgrid_tasks_lib'] diff --git a/src/conftest.py b/src/conftest.py new file mode 100644 index 0000000..f9e024d --- /dev/null +++ b/src/conftest.py @@ -0,0 +1,50 @@ +""" +This conftest mostly for handling warnings. Use the other conftest.py files for app/test config. + +- Filters for warnings that are triggered during import go at the top level. +- Filters for warnings thrown during test runs goes in pytest_configure() below. + +Having two conftest.py files is necessary because the warning configuration needs to happen before +the application's tests and/or code have a chance to import other libraries which may trigger +warnings. So this file remains a filesystem level above the "real" conftest.py which does all the +imports. +""" + +import warnings + + +# Treat any warning issued in a test as an exception so we are forced to explicitly handle or +# ignore it. +warnings.filterwarnings('error') +# Examples: +# warnings.filterwarnings( +# 'ignore', +# "'cgi' is deprecated and slated for removal in Python 3.13", +# category=DeprecationWarning, +# module='webob.compat', +# ) +# warnings.filterwarnings( +# 'ignore', +# "'crypt' is deprecated and slated for removal in Python 3.13", +# category=DeprecationWarning, +# module='passlib.utils', +# ) +########### +# REMINDER: when adding an ignore, add an issue to track it +########### + + +def pytest_configure(config): + """ + You may be able to do all your ignores above. If you find some warnings need to be ignored + in pytest, you can do that with something like: + + config.addinivalue_line( + 'filterwarnings', + # Note the lines that follow are implicitly concatinated, no "," at the end + 'ignore' + ':pythonjsonlogger.jsonlogger has been moved to pythonjsonlogger.json' + ':DeprecationWarning' + ':wtforms.meta', + ) + """ diff --git a/src/webgrid/__init__.py b/src/webgrid/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/webgrid/cli.py b/src/webgrid/cli.py new file mode 100644 index 0000000..19a5483 --- /dev/null +++ b/src/webgrid/cli.py @@ -0,0 +1,2 @@ +def main(): + print('Hello from', __name__) diff --git a/src/webgrid/version.py b/src/webgrid/version.py new file mode 100644 index 0000000..1f6518e --- /dev/null +++ b/src/webgrid/version.py @@ -0,0 +1 @@ +VERSION = '0.1.0' diff --git a/src/webgrid_tasks_lib.py b/src/webgrid_tasks_lib.py new file mode 100644 index 0000000..3f4f861 --- /dev/null +++ b/src/webgrid_tasks_lib.py @@ -0,0 +1,29 @@ +from collections.abc import Iterable +from os import environ +import subprocess + + +def sub_run( + *args, + capture=False, + returns: None | Iterable[int] = None, + **kwargs, +) -> subprocess.CompletedProcess: + kwargs.setdefault('check', not bool(returns)) + capture = kwargs.setdefault('capture_output', capture) + args = args + kwargs.pop('args', ()) + env = kwargs.pop('env', None) + if env: + kwargs['env'] = environ | env + if capture: + kwargs.setdefault('text', True) + + try: + result = subprocess.run(args, **kwargs) + if returns and result.returncode not in returns: + raise subprocess.CalledProcessError(result.returncode, args[0]) + return result + except subprocess.CalledProcessError as e: + if capture: + print(e.stderr) + raise diff --git a/src/webgrid_tests/test_cli.py b/src/webgrid_tests/test_cli.py new file mode 100644 index 0000000..60bfadc --- /dev/null +++ b/src/webgrid_tests/test_cli.py @@ -0,0 +1,5 @@ +from webgrid.cli import main + + +def test_main(): + main() diff --git a/tasks/bump b/tasks/bump new file mode 100755 index 0000000..d01b11e --- /dev/null +++ b/tasks/bump @@ -0,0 +1,80 @@ +#!/usr/bin/env python +# mise description="Bump version" +""" +Originates from https://github.com/level12/coppy + +Consider updating the source file in that repo with enhancements or bug fixes needed. +""" + +import datetime as dt + +import click + +from webgrid_tasks_lib import sub_run + + +def current_version(): + result = sub_run('hatch', 'version', capture=True) + return result.stdout.strip() + + +def date_version(current: str): + major, _, patch, *_ = current.split('.') + version = f'{major}.' + dt.date.today().strftime(r'%Y%m%d') + + patch = int(patch) + 1 if current.startswith(version) else 1 + + return f'{version}.{patch}' + + +@click.command() +@click.argument('kind', type=click.Choice(('micro', 'minor', 'major', 'date')), default='date') +@click.option('--show', is_flag=True, help="Only show next version, don't bump (date only)") +@click.option('--current', help='Simulate current version (date only)') +@click.option('--push/--no-push', help='Push after bump', default=True) +@click.pass_context +def main(ctx: click.Context, kind: str | None, show: bool, current: str | None, push: bool): + """ + Bump the version and (by default) git push including tags. + + Date based versioning is the default. Examples: + + v0.20231231.1 + v0.20231231.2 + v0.20240101.1 + + A normal bump will increment the minor or micro slot. Use a major bump when making breaking + changes in a library, e.g.: + + mise run bump major + Old: 0.20240515.1 + New: 1.0.0 + + Major, minor, and micro bumps are just passed through to `hatch version` and provided for + completeness. If using date based versioning only `date` and `major` need to be used. + + Assumes your project is using hatch-regex-commit or similar so that a commit & tag are created + automatically after every version bump. + """ + if show and kind != 'date': + ctx.fail('--show is only valid with date versioning') + if current and kind != 'date': + ctx.fail('--current is only valid with date versioning') + + if kind == 'date': + current = current or current_version() + version = date_version(current) + if show: + print('Current:', current) + print('Next:', version) + return + else: + version = kind + + sub_run('hatch', 'version', version) + if push: + sub_run('git', 'push', '--follow-tags') + + +if __name__ == '__main__': + main() From 4e03a08e9cbe929e50a65cb4b55a7f76d75e3127 Mon Sep 17 00:00:00 2001 From: Randy Syring Date: Sat, 2 Aug 2025 15:49:26 -0400 Subject: [PATCH 06/33] Move modules to ./src --- src/webgrid/__init__.py | 1899 +++++++++++++++++ {webgrid => src/webgrid}/blazeweb.py | 0 src/webgrid/cli.py | 2 - {webgrid => src/webgrid}/extensions.py | 0 {webgrid => src/webgrid}/filters.py | 0 {webgrid => src/webgrid}/flask.py | 0 {webgrid => src/webgrid}/i18n/babel.cfg | 0 .../webgrid}/i18n/es/LC_MESSAGES/webgrid.mo | Bin .../webgrid}/i18n/es/LC_MESSAGES/webgrid.po | 0 {webgrid => src/webgrid}/i18n/webgrid.pot | 0 {webgrid => src/webgrid}/renderers.py | 0 .../webgrid}/static/application_form_edit.png | Bin .../webgrid}/static/b_firstpage.png | Bin .../webgrid}/static/b_lastpage.png | Bin .../webgrid}/static/b_nextpage.png | Bin .../webgrid}/static/b_prevpage.png | Bin .../webgrid}/static/bd_firstpage.png | Bin .../webgrid}/static/bd_lastpage.png | Bin .../webgrid}/static/bd_nextpage.png | Bin .../webgrid}/static/bd_prevpage.png | Bin {webgrid => src/webgrid}/static/delete.png | Bin .../webgrid}/static/gettext.min.js | 0 .../static/i18n/es/LC_MESSAGES/webgrid.json | 0 .../webgrid}/static/jquery.multiple.select.js | 0 .../webgrid}/static/multiple-select.css | 0 .../webgrid}/static/multiple-select.png | Bin .../webgrid}/static/th_arrow_down.png | Bin .../webgrid}/static/th_arrow_up.png | Bin {webgrid => src/webgrid}/static/webgrid.css | 0 {webgrid => src/webgrid}/static/webgrid.js | 0 {webgrid => src/webgrid}/templates/grid.html | 0 .../webgrid}/templates/grid_footer.html | 0 .../webgrid}/templates/grid_header.html | 0 .../webgrid}/templates/grid_table.html | 0 .../webgrid}/templates/header_filtering.html | 0 .../webgrid}/templates/header_paging.html | 0 .../webgrid}/templates/header_sorting.html | 0 {webgrid => src/webgrid}/testing.py | 0 {webgrid => src/webgrid}/types.py | 0 {webgrid => src/webgrid}/utils.py | 0 {webgrid => src/webgrid}/validators.py | 0 src/webgrid/version.py | 2 +- .../webgrid_blazeweb_ta}/__init__.py | 0 .../webgrid_blazeweb_ta}/application.py | 0 .../webgrid_blazeweb_ta}/config/__init__.py | 0 .../webgrid_blazeweb_ta}/config/settings.py | 0 .../config/site_settings.py_tpl | 0 .../webgrid_blazeweb_ta}/extensions.py | 0 .../webgrid_blazeweb_ta}/i18n/babel.cfg | 0 .../es/LC_MESSAGES/webgrid_blazeweb_ta.mo | Bin .../es/LC_MESSAGES/webgrid_blazeweb_ta.po | 0 .../i18n/webgrid_blazeweb_ta.pot | 0 .../webgrid_blazeweb_ta/model}/__init__.py | 0 .../webgrid_blazeweb_ta}/model/orm.py | 0 .../webgrid_blazeweb_ta/tasks}/__init__.py | 0 .../webgrid_blazeweb_ta}/tasks/init_db.py | 0 .../templates/manage_people.html | 0 .../templates/nonstandard_header.css | 0 .../templates/nonstandard_header.html | 0 .../webgrid_blazeweb_ta/tests}/__init__.py | 0 .../webgrid_blazeweb_ta}/tests/grids.py | 0 .../webgrid_blazeweb_ta}/views.py | 0 .../tests => src/webgrid_ta}/__init__.py | 0 {webgrid_ta => src/webgrid_ta}/app.py | 0 .../webgrid_ta}/data/basic_table.html | 0 .../webgrid_ta}/data/people_table.html | 0 {webgrid_ta => src/webgrid_ta}/extensions.py | 0 {webgrid_ta => src/webgrid_ta}/grids.py | 0 {webgrid_ta => src/webgrid_ta}/helpers.py | 0 {webgrid_ta => src/webgrid_ta}/i18n/babel.cfg | 0 .../i18n/es/LC_MESSAGES/webgrid_ta.mo | Bin .../i18n/es/LC_MESSAGES/webgrid_ta.po | 0 .../webgrid_ta}/i18n/webgrid_ta.pot | 0 {webgrid_ta => src/webgrid_ta}/manage.py | 0 .../webgrid_ta}/model/__init__.py | 0 .../webgrid_ta}/model/entities.py | 0 .../webgrid_ta}/model/helpers.py | 0 .../webgrid_ta}/templates/groups.html | 0 .../webgrid_ta}/templates/index.html | 0 {webgrid_ta => src/webgrid_ta}/views.py | 0 {webgrid_ta => src/webgrid_tests}/__init__.py | 0 .../tests => src/webgrid_tests}/conftest.py | 0 .../webgrid_tests}/data/basic_table.html | 0 .../webgrid_tests}/data/people_table.html | 0 .../webgrid_tests}/data/stopwatch_table.html | 0 .../tests => src/webgrid_tests}/helpers.py | 0 .../tests => src/webgrid_tests}/test_api.py | 0 src/webgrid_tests/test_cli.py | 5 - .../webgrid_tests}/test_columns.py | 0 .../webgrid_tests}/test_filters.py | 0 .../webgrid_tests}/test_rendering.py | 0 .../webgrid_tests}/test_testing.py | 0 .../tests => src/webgrid_tests}/test_types.py | 0 .../tests => src/webgrid_tests}/test_unit.py | 0 webgrid/__init__.py | 1899 ----------------- webgrid/version.py | 1 - 96 files changed, 1900 insertions(+), 1908 deletions(-) rename {webgrid => src/webgrid}/blazeweb.py (100%) delete mode 100644 src/webgrid/cli.py rename {webgrid => src/webgrid}/extensions.py (100%) rename {webgrid => src/webgrid}/filters.py (100%) rename {webgrid => src/webgrid}/flask.py (100%) rename {webgrid => src/webgrid}/i18n/babel.cfg (100%) rename {webgrid => src/webgrid}/i18n/es/LC_MESSAGES/webgrid.mo (100%) rename {webgrid => src/webgrid}/i18n/es/LC_MESSAGES/webgrid.po (100%) rename {webgrid => src/webgrid}/i18n/webgrid.pot (100%) rename {webgrid => src/webgrid}/renderers.py (100%) rename {webgrid => src/webgrid}/static/application_form_edit.png (100%) rename {webgrid => src/webgrid}/static/b_firstpage.png (100%) rename {webgrid => src/webgrid}/static/b_lastpage.png (100%) rename {webgrid => src/webgrid}/static/b_nextpage.png (100%) rename {webgrid => src/webgrid}/static/b_prevpage.png (100%) rename {webgrid => src/webgrid}/static/bd_firstpage.png (100%) rename {webgrid => src/webgrid}/static/bd_lastpage.png (100%) rename {webgrid => src/webgrid}/static/bd_nextpage.png (100%) rename {webgrid => src/webgrid}/static/bd_prevpage.png (100%) rename {webgrid => src/webgrid}/static/delete.png (100%) rename {webgrid => src/webgrid}/static/gettext.min.js (100%) rename {webgrid => src/webgrid}/static/i18n/es/LC_MESSAGES/webgrid.json (100%) rename {webgrid => src/webgrid}/static/jquery.multiple.select.js (100%) rename {webgrid => src/webgrid}/static/multiple-select.css (100%) rename {webgrid => src/webgrid}/static/multiple-select.png (100%) rename {webgrid => src/webgrid}/static/th_arrow_down.png (100%) rename {webgrid => src/webgrid}/static/th_arrow_up.png (100%) rename {webgrid => src/webgrid}/static/webgrid.css (100%) rename {webgrid => src/webgrid}/static/webgrid.js (100%) rename {webgrid => src/webgrid}/templates/grid.html (100%) rename {webgrid => src/webgrid}/templates/grid_footer.html (100%) rename {webgrid => src/webgrid}/templates/grid_header.html (100%) rename {webgrid => src/webgrid}/templates/grid_table.html (100%) rename {webgrid => src/webgrid}/templates/header_filtering.html (100%) rename {webgrid => src/webgrid}/templates/header_paging.html (100%) rename {webgrid => src/webgrid}/templates/header_sorting.html (100%) rename {webgrid => src/webgrid}/testing.py (100%) rename {webgrid => src/webgrid}/types.py (100%) rename {webgrid => src/webgrid}/utils.py (100%) rename {webgrid => src/webgrid}/validators.py (100%) rename {webgrid/tests => src/webgrid_blazeweb_ta}/__init__.py (100%) rename {webgrid_blazeweb_ta => src/webgrid_blazeweb_ta}/application.py (100%) rename {webgrid_blazeweb_ta => src/webgrid_blazeweb_ta}/config/__init__.py (100%) rename {webgrid_blazeweb_ta => src/webgrid_blazeweb_ta}/config/settings.py (100%) rename {webgrid_blazeweb_ta => src/webgrid_blazeweb_ta}/config/site_settings.py_tpl (100%) rename {webgrid_blazeweb_ta => src/webgrid_blazeweb_ta}/extensions.py (100%) rename {webgrid_blazeweb_ta => src/webgrid_blazeweb_ta}/i18n/babel.cfg (100%) rename {webgrid_blazeweb_ta => src/webgrid_blazeweb_ta}/i18n/es/LC_MESSAGES/webgrid_blazeweb_ta.mo (100%) rename {webgrid_blazeweb_ta => src/webgrid_blazeweb_ta}/i18n/es/LC_MESSAGES/webgrid_blazeweb_ta.po (100%) rename {webgrid_blazeweb_ta => src/webgrid_blazeweb_ta}/i18n/webgrid_blazeweb_ta.pot (100%) rename {webgrid_blazeweb_ta => src/webgrid_blazeweb_ta/model}/__init__.py (100%) rename {webgrid_blazeweb_ta => src/webgrid_blazeweb_ta}/model/orm.py (100%) rename {webgrid_blazeweb_ta/model => src/webgrid_blazeweb_ta/tasks}/__init__.py (100%) rename {webgrid_blazeweb_ta => src/webgrid_blazeweb_ta}/tasks/init_db.py (100%) rename {webgrid_blazeweb_ta => src/webgrid_blazeweb_ta}/templates/manage_people.html (100%) rename {webgrid_blazeweb_ta => src/webgrid_blazeweb_ta}/templates/nonstandard_header.css (100%) rename {webgrid_blazeweb_ta => src/webgrid_blazeweb_ta}/templates/nonstandard_header.html (100%) rename {webgrid_blazeweb_ta/tasks => src/webgrid_blazeweb_ta/tests}/__init__.py (100%) rename {webgrid_blazeweb_ta => src/webgrid_blazeweb_ta}/tests/grids.py (100%) rename {webgrid_blazeweb_ta => src/webgrid_blazeweb_ta}/views.py (100%) rename {webgrid_blazeweb_ta/tests => src/webgrid_ta}/__init__.py (100%) rename {webgrid_ta => src/webgrid_ta}/app.py (100%) rename {webgrid_ta => src/webgrid_ta}/data/basic_table.html (100%) rename {webgrid_ta => src/webgrid_ta}/data/people_table.html (100%) rename {webgrid_ta => src/webgrid_ta}/extensions.py (100%) rename {webgrid_ta => src/webgrid_ta}/grids.py (100%) rename {webgrid_ta => src/webgrid_ta}/helpers.py (100%) rename {webgrid_ta => src/webgrid_ta}/i18n/babel.cfg (100%) rename {webgrid_ta => src/webgrid_ta}/i18n/es/LC_MESSAGES/webgrid_ta.mo (100%) rename {webgrid_ta => src/webgrid_ta}/i18n/es/LC_MESSAGES/webgrid_ta.po (100%) rename {webgrid_ta => src/webgrid_ta}/i18n/webgrid_ta.pot (100%) rename {webgrid_ta => src/webgrid_ta}/manage.py (100%) rename {webgrid_ta => src/webgrid_ta}/model/__init__.py (100%) rename {webgrid_ta => src/webgrid_ta}/model/entities.py (100%) rename {webgrid_ta => src/webgrid_ta}/model/helpers.py (100%) rename {webgrid_ta => src/webgrid_ta}/templates/groups.html (100%) rename {webgrid_ta => src/webgrid_ta}/templates/index.html (100%) rename {webgrid_ta => src/webgrid_ta}/views.py (100%) rename {webgrid_ta => src/webgrid_tests}/__init__.py (100%) rename {webgrid/tests => src/webgrid_tests}/conftest.py (100%) rename {webgrid/tests => src/webgrid_tests}/data/basic_table.html (100%) rename {webgrid/tests => src/webgrid_tests}/data/people_table.html (100%) rename {webgrid/tests => src/webgrid_tests}/data/stopwatch_table.html (100%) rename {webgrid/tests => src/webgrid_tests}/helpers.py (100%) rename {webgrid/tests => src/webgrid_tests}/test_api.py (100%) delete mode 100644 src/webgrid_tests/test_cli.py rename {webgrid/tests => src/webgrid_tests}/test_columns.py (100%) rename {webgrid/tests => src/webgrid_tests}/test_filters.py (100%) rename {webgrid/tests => src/webgrid_tests}/test_rendering.py (100%) rename {webgrid/tests => src/webgrid_tests}/test_testing.py (100%) rename {webgrid/tests => src/webgrid_tests}/test_types.py (100%) rename {webgrid/tests => src/webgrid_tests}/test_unit.py (100%) delete mode 100644 webgrid/__init__.py delete mode 100644 webgrid/version.py diff --git a/src/webgrid/__init__.py b/src/webgrid/__init__.py index e69de29..60ef4c8 100644 --- a/src/webgrid/__init__.py +++ b/src/webgrid/__init__.py @@ -0,0 +1,1899 @@ +from __future__ import absolute_import +import datetime as dt +import inspect +import logging +import sys +import six +import time +import urllib.parse + +from blazeutils.containers import HTMLAttributes +from blazeutils.datastructures import BlankObject, OrderedDict +from blazeutils.helpers import tolist +from blazeutils.numbers import decimalfmt +from blazeutils.strings import case_cw2us, randchars +from blazeutils.spreadsheets import xlsxwriter, openpyxl +import sqlalchemy as sa +import sqlalchemy.sql as sasql + +from . import validators +from .extensions import gettext as _ +from .renderers import HTML, XLSX +from .version import VERSION as __version__ # noqa: F401 + +# conditional imports to support libs without requiring them +try: + import arrow +except ImportError: + arrow = None + +log = logging.getLogger(__name__) + + +# subtotals functions +sum_ = sasql.functions.sum +avg_ = sasql.func.avg + + +def subtotal_function_map(v): + """Maps string value to a function, or passes the value through. + + Recognizes True, "sum" or "avg". If True, "sum" is used as the default + subtotal function. + + Args: + v (Union(str, callable)): Value defining the subtotal method. + + Returns: + Union(str, callable): `sum` or `avg` SQLAlchemy functions, or the value. + """ + if v is True or v == 'sum': + return sum_ + elif v == 'avg': + return avg_ + return v + + +class _None(object): + """ + A sentinal object to indicate no value + """ + pass + + +class ExtractionError(TypeError): + """ raised when we are unable to extract a value from the record """ + pass + + +class _DeclarativeMeta(type): + + def __new__(cls, name, bases, class_dict): + class_dict['_rowstylers'] = [] + class_dict['_colstylers'] = [] + class_dict['_colfilters'] = [] + class_columns = [] + + # add columns from base classes + for base in bases: + base_columns = getattr(base, '__cls_cols__', ()) + class_columns.extend(base_columns) + class_columns.extend(class_dict.get('__cls_cols__', ())) + class_dict['__cls_cols__'] = class_columns + + # we have to assign the attribute name + for k, v in six.iteritems(class_dict): + # catalog the row stylers + if getattr(v, '__grid_rowstyler__', None): + class_dict['_rowstylers'].append(v) + + # catalog the column stylers + for_column = getattr(v, '__grid_colstyler__', None) + if for_column: + class_dict['_colstylers'].append((v, for_column)) + + # catalog the column filters + for_column = getattr(v, '__grid_colfilter__', None) + if for_column: + class_dict['_colfilters'].append((v, for_column)) + + return super(_DeclarativeMeta, cls).__new__(cls, name, bases, class_dict) + + +class Column(object): + """Column represents the data and render specification for a table column. + + Args: + label (str): Label to use for filter/sort selection and table header. + + key (Union[Expression, str], optional): Field key or SQLAlchemy expression. + If an expression is provided, column attempts to derive a string key name from + the expression. Defaults to None. + + filter (FilterBase, optional): Filter class or instance. Defaults to None. + + can_sort (bool, optional): Enables column for selection in sort keys. Defaults to True. + + xls_num_format (str, optional): XLSX number/date format. Defaults to None. + + render_in (Union(list(str), callable), optional): Targets to render as a column. + Defaults to _None. + + has_subtotal (Union(bool,str,callable), optional): Subtotal method to use, if any. + True or "sum" will yield a sum total. "avg" maps to average. Can also be a + callable that will be called with the aggregate expression and is expected + to return a SQLAlchemy expression. Defaults to False. + + visible (Union(bool, callable), optional): Enables any target in `render_in`. + Defaults to True. + + group (ColumnGroup, optional): Render grouping under a single heading. Defaults to None. + + Class Attributes: + xls_width (float, optional): Override to autocalculated width in Excel exports. + + xls_num_format (str, optional): Default numeric/date format type. + """ + xls_width = None + xls_num_format = None + json_type_helper = None + _render_in = 'html', 'xlsx', 'csv', 'json' + _visible = True + + @property + def render_in(self): + """Target(s) in which the field should be rendered as a column. + + Can be set to a callable, which will be called with the column instance. + + Returns: + tuple(str): Renderer identifiers. + """ + resolved = self._render_in + if callable(resolved): + resolved = resolved(self) + return tuple(tolist(resolved)) + + @render_in.setter + def render_in(self, val): + self._render_in = val + + @property + def visible(self): + """Enables column to be rendered to any target in `render_in`. + + Can be set to a callable, which will be called with the column instance. + + Returns: + bool: Enable render. + """ + resolved = self._visible + if callable(resolved): + resolved = resolved(self) + return resolved + + @visible.setter + def visible(self, val): + self._visible = val + + def __new__(cls, *args, **kwargs): + col_inst = super(Column, cls).__new__(cls) + if '_dont_assign' not in kwargs: + col_inst._assign_to_grid() + return col_inst + + def _assign_to_grid(self): + """Columns being set up in declarative fashion need to be attached to the class + somewhere. In WebGrid, we have a class attribute `__cls_cols__` that columns + append themselves to. Subclasses, use of mixins, etc. will combine these column + lists elsewhere. + """ + grid_locals = sys._getframe(2).f_locals + grid_cls_cols = grid_locals.setdefault('__cls_cols__', []) + grid_cls_cols.append(self) + + def __init__(self, label, key=None, filter=None, can_sort=True, # noqa: C901 + xls_width=None, xls_num_format=None, render_in=_None, has_subtotal=False, + visible=True, group=None, **kwargs): + self.label = label + self.key = key + self.filter = filter + self.filter_for = None + self.filter_op = None + self._create_order = False + self.can_sort = can_sort + self.has_subtotal = has_subtotal + self.kwargs = kwargs + self.grid = None + self.expr = None + self._query_idx = None + self._query_key = None + if render_in is not _None: + self.render_in = render_in + self.visible = visible + if xls_width: + self.xls_width = xls_width + if xls_num_format: + self.xls_num_format = xls_num_format + + try: + is_group_cls = issubclass(type(group), ColumnGroup) or issubclass(group, ColumnGroup) + except TypeError: + is_group_cls = False + + if group is not None and not is_group_cls: + raise ValueError(_('expected group to be a subclass of ColumnGroup')) + + self.group = group + + # if the key isn't a base string, assume its a column-like object that + # works with a SA Query instance + if key is None: + self.can_sort = False + elif not isinstance(key, six.string_types): + self.expr = col = key + # use column.key, column.name, or None in that order + key = getattr(col, 'key', getattr(col, 'name', None)) + + if key is None: + raise ValueError(_('expected filter to be a SQLAlchemy column-like' + ' object, but it did not have a "key" or "name"' + ' attribute')) + self.key = self._query_key = key + + # filters can be sent in as a class (not class instance) if needed + if inspect.isclass(filter): + if self.expr is None: + raise ValueError(_('the filter was a class type, but no' + ' column-like object is available from "key" to pass in as' + ' as the first argument')) + self.filter = filter(self.expr) + + def new_instance(self, grid): + """Create a "copy" instance that is linked to a grid instance. + + Used during the grid instantiation process. Grid classes have column instances defining + the grid structure. When the grid instantiates, we have to copy those column instances + along with it, to attach them to the grid instance. + """ + cls = self.__class__ + key = grid.get_unique_column_key( + self.key + or case_cw2us(str(self.label).replace(' ', '')) + or 'unnamed_expression' + ) + + column = cls(self.label, key, None, self.can_sort, group=self.group, _dont_assign=True) + column.key = key + column.grid = grid + column.expr = self.expr + column._query_key = self._query_key + + if self.filter: + column.filter = self.filter.new_instance( + dialect=grid.manager.db.engine.dialect, col=column.expr + ) + + column.head = BlankObject() + column.head.hah = HTMLAttributes(self.kwargs) + column.body = BlankObject() + column.body.hah = HTMLAttributes(self.kwargs) + + # try to be smart about which attributes should get copied to the + # new instance by looking for attributes on the class that have the + # same name as arguments to the classes __init__ method + args = (inspect.getargspec(self.__init__).args + if six.PY2 else inspect.getfullargspec(self.__init__).args) + + for argname in args: + if argname != 'self' and argname not in ( + 'label', 'key', 'filter', 'can_sort', 'render_in', 'visible' + ) and hasattr(self, argname): + setattr(column, argname, getattr(self, argname)) + + # Copy underlying value of render_in and visible, in case they are + # lambdas that should be called per grid instance. + column.render_in = self._render_in + column.visible = self._visible + + return column + + def extract_and_format_data(self, record): + """ + Extract a value from the record for this column and run it through + the data formaters. + """ + data = self.extract_data(record) + data = self.format_data(data) + for _filter, cname in self.grid._colfilters: + for_column = self.grid.column(cname) + if self.key == for_column.key: + data = _filter(self.grid, data) + return data + + def extract_data(self, record): # noqa: C901 + """ + Locate the data for this column in the record and return it. + """ + # key style based on key + try: + if isinstance(record, dict): + return record[self.key] + return record._mapping[self.key] + except (TypeError, KeyError, AttributeError): + pass + + # index style based on position in query and key + if ( + self._query_idx is not None + and hasattr(record, '_fields') + ): + try: + if record._fields[self._query_idx] == self._query_key: + return record[self._query_idx] + except IndexError: + pass + + # attribute style + try: + return getattr(record, self._query_key) + except AttributeError as e: + if ("object has no attribute '%s'" % self._query_key) not in str(e): + raise + except TypeError as e: + if 'attribute name must be string' not in str(e): + raise + + # attribute style with grid key + try: + return getattr(record, self.key) + except AttributeError as e: + if ("object has no attribute '%s'" % self.key) not in str(e): + raise + + raise ExtractionError(_('key "{key}" not found in record', key=self.key)) + + def format_data(self, value): + """ + Use to adjust the value extracted from the record for this column. + By default, no change is made. Useful in sub-classes. + """ + return value + + def render(self, render_type, record, *args, **kwargs): + """Entrypoint from renderer. + + Uses any renderer-specific overrides from the column, or else falls back to + the output of `extract_and_format_data`. + + Renderer-specific methods are expected to be named `render_`, + e.g. `render_html` or `render_xlsx`. + """ + render_attr = 'render_{0}'.format(render_type) + if hasattr(self, render_attr): + return getattr(self, render_attr)(record, *args, **kwargs) + return self.extract_and_format_data(record) + + def apply_sort(self, query, flag_desc): + """Query modifier to enable sort for this column's expression.""" + if self.expr is None: + direction = 'DESC' if flag_desc else 'ASC' + return query.order_by(sasql.text('{0} {1}'.format(self.key, direction))) + if flag_desc: + return query.order_by(self.expr.desc()) + return query.order_by(self.expr) + + def __repr__(self): + return ''.format(self) + + def xls_width_calc(self, value): + """Calculate a width to use for an Excel renderer. + + Defaults to the `xls_width` attribute, if it is set to a non-zero value. Otherwise, + use the length of the stringified value. + """ + if self.xls_width: + return self.xls_width + if isinstance(value, six.string_types): + return len(value) + return len(str(value)) + + +class LinkColumnBase(Column): + """Base class for columns rendering as links in HTML. + + Expects a subclass to supply a `create_url` method for defining the link target. + + Notable args: + link_label (str, optional): Caption to use instead of extracted data from the record. + + Class attributes: + link_attrs (dict): Additional attributes to render on the A tag. + + """ + link_attrs = {} + + def __init__(self, label, key=None, filter=None, can_sort=True, + link_label=None, xls_width=None, xls_num_format=None, + render_in=_None, has_subtotal=False, visible=True, group=None, **kwargs): + super().__init__(label, key, filter, can_sort, xls_width, + xls_num_format, render_in, has_subtotal, visible, + group=group, **kwargs) + self.link_label = link_label + + def link_to(self, label, url, **kwargs): + """Basic render of an anchor tag.""" + return self.grid.html._render_jinja( + '{{label}}', + url=url, + attrs=kwargs, + label=label + ) + + def render_html(self, record, hah): + """Renderer override for HTML to set up a link rather than using the raw data value.""" + url = self.create_url(record) + if self.link_label is not None: + label = self.link_label + else: + label = self.extract_and_format_data(record) + return self.link_to(label, url, **self.link_attrs) + + def create_url(self, record): + """Generate a URL from the given record. + + Expected to be overridden in subclass. + """ + raise NotImplementedError('create_url() must be defined on a subclass') + + +class BoolColumn(Column): + """Column rendering values as True/False (or the given labels). + + Notable args: + reverse (bool, optional): Switch true/false cases. + + true_label (str, optional): String to use for the true case. + + false_label (str, optional): String to use for the false case. + """ + json_type_helper = 'boolean' + + def __init__(self, label, key_or_filter=None, key=None, can_sort=True, + reverse=False, true_label=_('True'), false_label=_('False'), + xls_width=None, xls_num_format=None, render_in=_None, has_subtotal=False, + visible=True, group=None, **kwargs): + super().__init__(label, key_or_filter, key, can_sort, xls_width, + xls_num_format, render_in, + has_subtotal, visible, group=group, **kwargs) + self.reverse = reverse + self.true_label = true_label + self.false_label = false_label + + def format_data(self, data): + if self.reverse: + data = not data + if data: + return self.true_label + return self.false_label + + +class YesNoColumn(BoolColumn): + """BoolColumn rendering values as Yes/No. + + Notable args: + reverse (bool, optional): Switch true/false cases. + + """ + + def __init__(self, label, key_or_filter=None, key=None, can_sort=True, + reverse=False, xls_width=None, xls_num_format=None, render_in=_None, + has_subtotal=False, visible=True, group=None, **kwargs): + super().__init__(label, key_or_filter, key, can_sort, reverse, + _('Yes'), _('No'), xls_width, xls_num_format, + render_in, has_subtotal, visible, group=group, **kwargs) + + +class DateColumnBase(Column): + """Base column for rendering date values in specified formats. + + Designed to work with Python date/datetime/time and Arrow. + + Notable args/attributes: + html_format (str, optional): Date format string for HTML. + + csv_format (str, optional): Date format string for CSV. + + xls_num_format (str, optional): Date format string for Excel. + + """ + + def __init__(self, label, key_or_filter=None, key=None, can_sort=True, + html_format=None, csv_format=None, xls_width=None, + xls_num_format=None, render_in=_None, has_subtotal=False, visible=True, group=None, + **kwargs): + super().__init__(label, key_or_filter, key, can_sort, xls_width, + xls_num_format, render_in, has_subtotal, + visible, group=group, **kwargs) + if html_format: + self.html_format = html_format + + if csv_format: + self.csv_format = csv_format + + def _format_datetime(self, data, format): + # if we have an arrow date, allow html_format to use that functionality + if arrow and isinstance(data, arrow.Arrow): + if data.strftime(format) == format: + return data.format(format) + return data.strftime(format) + + def render_html(self, record, hah): + data = self.extract_and_format_data(record) + if not data: + return data + return self._format_datetime(data, self.html_format) + + def render_xlsx(self, record): + data = self.extract_and_format_data(record) + if not data: + return data + # if we have an arrow date, pull the underlying datetime, else the renderer won't know + # how to handle it + if arrow and isinstance(data, arrow.Arrow): + data = data.datetime + # Excel can't use timezone data, so strip it out + if isinstance(data, dt.datetime) and data.tzinfo is not None: + data = data.replace(tzinfo=None) + return data + + def render_csv(self, record): + data = self.extract_and_format_data(record) + if not data: + return data + return self._format_datetime(data, self.csv_format) + + def xls_width_calc(self, value): + """Determine approximate width from value. + + Value will be a date or datetime object, format as if it was going + to be in HTML as an approximation of its format in Excel. + """ + if self.xls_width: + return self.xls_width + try: + html_version = value.strftime(self.html_format) + return len(html_version) + except AttributeError as e: + if "has no attribute 'strftime'" not in str(e): + raise + # must be the column heading + return Column.xls_width_calc(self, value) + + +class DateColumn(DateColumnBase): + """Column for rendering date values in specified formats. + + Designed to work with Python date and Arrow. + + Notable args/attributes: + html_format (str, optional): Date format string for HTML. + + csv_format (str, optional): Date format string for CSV. + + xls_num_format (str, optional): Date format string for Excel. + + """ + # !!!: localize + html_format = '%m/%d/%Y' + csv_format = '%Y-%m-%d' + xls_num_format = 'm/dd/yyyy' + json_type_helper = 'date' + + +class DateTimeColumn(DateColumnBase): + """Column for rendering datetime values in specified formats. + + Designed to work with Python datetime and Arrow. + + Notable args/attributes: + html_format (str, optional): Date format string for HTML. + + csv_format (str, optional): Date format string for CSV. + + xls_num_format (str, optional): Date format string for Excel. + + """ + # !!!: localize + html_format = '%m/%d/%Y %I:%M %p' + csv_format = '%Y-%m-%d %H:%M:%S%z' + xls_num_format = 'mm/dd/yyyy hh:mm am/pm' + json_type_helper = 'datetime' + + +class TimeColumn(DateColumnBase): + """Column for rendering time values in specified formats. + + Designed to work with Python time and Arrow. + + Notable args/attributes: + html_format (str, optional): Date format string for HTML. + + csv_format (str, optional): Date format string for CSV. + + xls_num_format (str, optional): Date format string for Excel. + + """ + # !!!: localize + html_format = '%I:%M %p' + csv_format = '%H:%M' + xls_num_format = 'hh:mm am/pm' + json_type_helper = 'time' + + +class NumericColumn(Column): + """Column for rendering formatted number values. + + Notable args: + format_as (str, optional): Generic formats. Default "general". + - general: thousands separator and decimal point + - accounting: currency symbol, etc. + - percent: percentage symbol, etc. + + places (int, optional): Decimal places to round to for general. Default 2. + + curr (str, optional): Currency symbol for general. Default empty string. + + sep (str, optional): Thousands separator. Default empty string. + + dp (str, optional): Decimal separator. Default empty string. + + pos (str, optional): Positive number indicator. Default empty string. + + neg (str, optional): Negative number indicator for general. Default empty string. + + trailneg (str, optional): Negative number suffix. Default empty string. + + xls_neg_red (bool, optional): Renders negatives in red for Excel. Default True. + + Class attributes: + `xls_fmt_general`, `xls_fmt_accounting`, `xls_fmt_percent` are Excel number + formats used for the corresponding `format_as` setting. + """ + # !!!: localize + xls_fmt_general = '#,##0{dec_places};{neg_prefix}-#,##0{dec_places}' + xls_fmt_accounting = '_($* #,##0{dec_places}_);{neg_prefix}_($* (#,##0{dec_places})' + \ + ';_($* "-"??_);_(@_)' + xls_fmt_percent = '0{dec_places}%;{neg_prefix}-0{dec_places}%' + + def __init__(self, label, key_or_filter=None, key=None, can_sort=True, + reverse=False, xls_width=None, xls_num_format=None, + render_in=_None, format_as='general', places=2, curr='', + sep=',', dp='.', pos='', neg='-', trailneg='', + xls_neg_red=True, has_subtotal=False, visible=True, group=None, **kwargs): + super().__init__(label, key_or_filter, key, can_sort, xls_width, + xls_num_format, render_in, + has_subtotal, visible, group=group, **kwargs) + self.places = places + self.curr = curr + self.sep = sep + self.dp = dp + self.pos = pos + self.neg = neg + self.trailneg = trailneg + self.xls_neg_red = xls_neg_red + self.format_as = format_as + self.json_type_helper = f'number_{format_as}' + + def html_decimal_format_opts(self, data): + """Return tuple of options to expand for decimalfmt arguments. + + `places`, `curr`, `neg`, and `trailneg` attributes are passed through unless `format_as` + is "accounting". + """ + return ( + 2 if self.format_as == 'accounting' else self.places, + '$' if self.format_as == 'accounting' else self.curr, + self.sep, + self.dp, + self.pos, + '(' if self.format_as == 'accounting' else self.neg, + ')' if self.format_as == 'accounting' else self.trailneg, + ) + + def render_html(self, record, hah): + """HTML render override for numbers. + + If format is percent, the value is multiplied by 100 to get the render value. + + Negative values are given a "negative" CSS class in the render. + """ + data = self.extract_and_format_data(record) + if not data and data != 0: + return data + + if self.format_as == 'percent': + data = data * 100 + + formatted = decimalfmt(data, *self.html_decimal_format_opts(data)) + + if self.format_as == 'percent': + formatted += '%' + + if data < 0: + hah.class_ += 'negative' + + return formatted + + def xls_construct_format(self, fmt_str): + """Apply places and xls_neg_red settings to the given number format string.""" + neg_prefix = '[RED]' if self.xls_neg_red else '' + dec_places = '.'.ljust(self.places + 1, '0') if self.places else '' + return fmt_str.format(dec_places=dec_places, neg_prefix=neg_prefix) + + def get_num_format(self): + """Match format_as setting to one of the format strings in class attributes.""" + if self.format_as == 'general': + return self.xls_construct_format(self.xls_fmt_general) + if self.format_as == 'percent': + return self.xls_construct_format(self.xls_fmt_percent) + if self.format_as == 'accounting': + return self.xls_construct_format(self.xls_fmt_accounting) + return None + + @property + def xlsx_style(self): + """Number format for XLSX target.""" + return { + 'num_format': self.get_num_format() + } + + +class EnumColumn(Column): + """ + This column type is meant to be used with python `enum.Enum` type columns. It expects that + the display value is the `value` attribute of the enum instance. + """ + def format_data(self, value): + if value is None: + return None + return value.value + + +class ColumnGroup(object): + r"""Represents a grouping of grid columns which may be rendered within a group label. + + Args: + label (str): Grouping label to be rendered for the column set. + class\_ (str): CSS class name to apply in HTML rendering. + """ # noqa: W605 + label = None + class_ = None + + def __init__(self, label, class_=None): + self.label = label + self.class_ = class_ + + +class QueryStringBuilder: + arg_factories = ( + 'session', + 'search', + 'paging', + 'sort', + 'filter', + ) + + def __init__(self, grid, include_session=False): + self.grid = grid + self.include_session = include_session + + def __call__(self): + return self.build() + + def args_session(self): + if not self.include_session: + return [] + return [('session_key', self.grid.session_key)] + + def args_search(self): + if not self.grid.search_value: + return [] + return [('search', self.grid.search_value)] + + def args_paging(self): + grid_args = [] + if self.grid.on_page != self.grid.__class__.on_page: + grid_args.append(('onpage', self.grid.on_page)) + if self.grid.per_page != self.grid.__class__.per_page: + grid_args.append(('perpage', self.grid.per_page)) + return grid_args + + def args_sort(self): + # sort args are stored as tuple of (key, flag_desc) + return map( + lambda item: (f'sort{item[0]}', ('-' if item[1][1] else '') + item[1][0]), + enumerate(self.grid.order_by, 1), + ) + + def args_filter(self): + # for any filters, we only want to include args if the filter is set + grid_args = [] + for col in self.grid.filtered_cols.values(): + _filter = col.filter + if not _filter.is_active: + continue + grid_args.append((f'op({col.key})', _filter.op)) + if _filter.value1: + grid_args.extend(map( + lambda item: (f'v1({col.key})', item), + tolist(_filter.value1), + )) + if _filter.value2: + grid_args.append((f'v2({col.key})', _filter.value2)) + return grid_args + + def build(self): + grid_args = [] + for _factory_key in self.arg_factories: + grid_args.extend(getattr(self, f'args_{_factory_key}')()) + + # note: sorting is not strictly necessary but ensures order of keys for testing + return urllib.parse.urlencode(sorted(grid_args), safe='()') + + +class BaseGrid(six.with_metaclass(_DeclarativeMeta, object)): + r"""WebGrid grid base class. + + Handles class declarative-style grid description of columns, filterers, and rendering. + + The constructor is responsible for: + + - setting initial attributes + - initializing renderers + - setting up columns for the grid instance + - running the grid's `post_init` method + + Args: + ident (str, optional): Identifier value for ident instance property. Defaults to None. + + per_page (int, optional): Default number of records per page. Defaults to _None. + + on_page (int, optional): Default starting page. Defaults to _None. + + qs_prefix (str, optional): Arg name prefix to apply in query string. Useful for having + multiple unconnected grids on a single page. Defaults to ''. + + class\_ (str, optional): CSS class name for main grid div. Defaults to 'datagrid'. + + Class Attributes: + identifier (str): Optional string identifier used for the ident property. + + sorter_on (bool): Enable HTML sorting UX. Default True. + + pager_on (bool): Enable record limits in queries and HTML pager UX. Default True. + + per_page (int): Default number of records per page, can be overridden in constructor + or through query string args. Default 50. + + on_page (int): Default page number, can be overridden in constructor or through + query string args. Default 1. + + hide_controls_box (bool): Hides HTML filter/page/sort/count UX. Default False. + + session_on (bool): Enable web context session storage of grid filter/page/sort args. + Default True. + + subtotals (string): Enable subtotals. Can be none|page|grand|all. Default "none". + + manager (Manager): Framework plugin for the web context, such as webgrid.flask.WebGrid. + + allowed_export_targets (dict[str, Renderer]): Map non-HTML export targets to the + Renderer classes. + + enable_search (bool): Enable single-search UX. Default True. + + unconfirmed_export_limit (int): Ask for confirmation before exporting more than this many + records. Set to None to disable. Default 10000. + + query_select_from (selectable): Entity, table, or other selectable(s) to use as the query + from. If attributes like query_filter are used along with select_from, SQLAlchemy may + require the select_from to precede the filtering. + + query_joins (tuple): Tuple of joins to bring the query together for all columns. May + have just the join object, or also conditions. + e.g. [Blog], ([Blog.category], ), or [(Blog, Blog.active == sa.true())] + Note, relationship attributes must be referenced within tuples, due to SQLAlchemy magic. + + query_outer_joins (tuple): Tuple of outer joins. See query_joins. + + query_filter (tuple): Filter parameter(s) tuple to be used on the query. + Note, relationship attributes must be referenced within tuples, due to SQLAlchemy magic. + + query_default_sort (tuple): Parameter(s) tuple to be passed to order_by if sort options + are not set on the grid. + Note, relationship attributes must be referenced within tuples, due to SQLAlchemy magic. + + """ # noqa: W605 + __cls_cols__ = () + identifier = None + sorter_on = True + pager_on = True + per_page = 50 + on_page = 1 + hide_controls_box = False + # enables keyed session store of grid arguments + session_on = True + # enables page/grand subtotals: none|page|grand|all + subtotals = 'none' + manager = None + allowed_export_targets = None + # Enables single-search feature, where one search value is applied to every supporting + # filter at once + enable_search = True + + # Base selectable(s) to be used in the FROM clause of the query + query_select_from = None + # List of joins to bring the query together for all columns. May have just the join object, + # or also conditions + # e.g. [Blog], ([Blog.category], ), or [(Blog, Blog.active == sa.true())] + # note: relationship attributes must be referenced within tuples, due to SQLAlchemy magic + query_joins = None + query_outer_joins = None + # Filter parameter(s) tuple to be used on the query + # note: relationship attributes must be referenced within tuples, due to SQLAlchemy magic + query_filter = None + # Parameter(s) tuple to be passed to order_by if sort options are not set on the grid + # note: relationship attributes must be referenced within tuples, due to SQLAlchemy magic + query_default_sort = None + + # Will ask for confirmation before exporting more than this many records. + # Set to None to disable this check + unconfirmed_export_limit = 10000 + + def __init__(self, ident=None, per_page=_None, on_page=_None, qs_prefix='', class_='datagrid', + **kwargs): + self._ident = ident + self.hah = HTMLAttributes(kwargs) + self.hah.id = self.ident + self.hah.class_ += class_ + self.filtered_cols = OrderedDict() + self.subtotal_cols = OrderedDict() + self.order_by = [] + self.qs_prefix = qs_prefix + self.user_warnings = [] + self.search_value = None + self._record_count = None + self._records = None + self._page_totals = None + self._grand_totals = None + + if self.allowed_export_targets is None: + self.allowed_export_targets = {} + # If the grid doesn't define any export targets + # lets setup the export target for xlsx if we have the requirement + if openpyxl or xlsxwriter: + self.allowed_export_targets['xlsx'] = XLSX + self.set_renderers() + self.export_to = None + # when session feature is enabled, key is the unique string + # used to distinguish grids. Initially set to a random + # string, but will be set to the session key in args + self.session_key = randchars(12) + # at times, different grids may be made to share a session + self.foreign_session_loaded = False + + self.per_page = per_page if per_page is not _None else self.__class__.per_page + self.on_page = on_page if on_page is not _None else self.__class__.on_page + + self.columns = [] + self.key_column_map = {} + + self._init_columns() + self.post_init() + + def _init_columns(self): + """Create column instances to attach to a grid instance. + + Columns set up in the declarative grid description are instances bound to the grid + class. When the grid is instantiated, those column instances need to be copied over + to the grid instance. + + Columns are responsible for their own "copy" process with the `new_instance` method. + """ + for col in self.__cls_cols__: + self.add_column(col) + + def add_column(self, column): + new_col = column.new_instance(self) + self.columns.append(new_col) + self.key_column_map[new_col.key] = new_col + if new_col.filter is not None: + self.filtered_cols[new_col.key] = new_col + if new_col.has_subtotal is not False and new_col.has_subtotal is not None: + self.subtotal_cols[new_col.key] = ( + subtotal_function_map(new_col.has_subtotal), + new_col + ) + + def drop_columns(self, column_keys): + self.columns = list(filter( + lambda col: col.key not in tolist(column_keys), + self.columns + )) + for key in tolist(column_keys): + self.key_column_map.pop(key, None) + self.filtered_cols.pop(key, None) + self.subtotal_cols.pop(key, None) + + def post_init(self): + """Provided for subclasses to run post-initialization customizations. + """ + pass + + def set_column_order(self, column_keys): + """Most renderers output columns in the order they appear in the grid's ``columns`` + list. When bringing mixins together or subclassing a grid, however, the order is + often not what is intended. + + This method allows a manual override of column order, based on keys.""" + key_check = set(column_keys) - set(self.key_column_map.keys()) + if key_check: + raise Exception(f'Keys not recognized on grid: {key_check}') + + self.columns = [ + self.key_column_map[key] for key in column_keys + ] + + def before_query_hook(self): + """Hook to give subclasses a chance to change things before executing the query. + """ + pass + + def build(self, grid_args=None): + """Apply query args, run `before_query_hook`, and execute a record count query. + + Calling `build` is preferred to simply calling `apply_qs_args` in a view. Otherwise, + AttributeErrors can be hidden when the grid is used in Jinja templates. + """ + self.apply_qs_args(grid_args=grid_args) + self.before_query_hook() + # this will force the query to execute. We used to wait to evaluate this but it ended + # up causing AttributeErrors to be hidden when the grid was used in Jinja. + # Calling build is now preferred over calling .apply_qs_args() and then .html() + self.record_count + + def check_auth(self): + """For API usage, provides a hook for grids to specify authorization that should be + applied for the API responder method. + + If a 40* response is warranted, take that action here. + + Note, this method is not part of normal grid/render operation. It will only be + executed if run by a calling layer, such as the Flask WebGridAPI manager/extension. + """ + pass + + def column(self, ident): + """Retrieve a grid column instance via either the key string or index int. + + Args: + ident (Union[str, int]): Key/index for lookup. + + Returns: + Column: Instance column matching the ident. + + Raises: + KeyError when ident is a string not matching any column. + + IndexError when ident is an int but out of bounds for the grid. + """ + if isinstance(ident, six.string_types): + return self.key_column_map[ident] + return self.columns[ident] + + def has_column(self, ident): + """Verify string key or int index is defined for the grid instance. + + Args: + ident (Union[str, int]): Key/index for lookup. + + Returns: + bool: Indicates whether key/index is in the grid columns. + + """ + if ident is None: + return False + if isinstance(ident, six.string_types): + return ident in self.key_column_map + return 0 <= ident < len(self.columns) + + def get_unique_column_key(self, key): + """Apply numeric suffix to a field key to make the key unique to the grid. + + Helpful for when multiple entities are represented in grid columns but have + the same field names. + + For instance, Blog.label and Author.label both have the field name `label`. + The first column will have the `label` key, and the second will get `label_1`. + + Args: + key (str): field key to make unique. + + Returns: + str: unique key that may be assigned in the grid's `key_column_map`. + """ + suffix_counter = 0 + new_key = key + while self.has_column(new_key): + suffix_counter += 1 + new_key = '{}_{}'.format(key, suffix_counter) + return new_key + + def iter_columns(self, render_type): + """Generator yielding columns that are visible and enabled for target `render_type`. + + Args: + render_type (str): [description] + + Yields: + Column: Grid instance's column instance that is renderable for `render_type`. + """ + for col in self.columns: + if col.visible and render_type in col.render_in: + yield col + + def can_search(self): + """Grid `enable_search` attr turns on search, but check if there are supporting filters. + + Returns: + bool: search enabled and supporting filters exist + """ + return self.enable_search and len(self.search_expression_generators) > 0 + + @property + def search_expression_generators(self): + """Get single-search query modifier factories from the grid filters. + + Raises: + Exception: filter's `get_search_expr` did not return None or callable + + Returns: + tuple(callable): search expression callables from grid filters + """ + is_aggregate = self.search_uses_aggregate + + # We filter out None here so as to disregard filters that don't support the search feature. + def check_expression_generator(expr_gen): + if expr_gen is not None and not callable(expr_gen): + raise Exception( + 'bad filter search expression: {} is not callable'.format(str(expr_gen)) + ) + return expr_gen is not None + + # Also conditionally filter aggregate/non-aggregate so we're not mixing expression types. + return tuple(filter( + check_expression_generator, + [col.filter.get_search_expr() for col in self.filtered_cols.values() + if col.filter.is_aggregate is is_aggregate] + )) + + @property + def search_uses_aggregate(self): + """Determine whether search should use aggregate filtering. + + By default, only use the HAVING clause if all search-enabled filters are marked + as aggregate. Otherwise, we'd be requiring all grid columns to be in query + grouping. If there are filters for search that are not aggregate, the grid will + only search on the non-aggregate columns. + + Returns: + bool: search aggregate usage determined from filter info + """ + has_search = False + for col in self.filtered_cols.values(): + if col.filter.get_search_expr() is not None: + has_search = True + if not col.filter.is_aggregate: + return False + return has_search + + def set_renderers(self): + """Renderers assigned as attributes on the grid instance, named by render target. + """ + self.html = HTML(self) + for key, value in self.allowed_export_targets.items(): + setattr(self, key, value(self)) + + def set_filter(self, key, op, value, value2=None): + """Set filter parameters on a column's filter. Resets record cache. + + Args: + key (str): Column identifier + op (str): Operator + value (Any): First filter value + value2 (Any, optional): Second filter value if applicable. Defaults to None. + """ + self.clear_record_cache() + self.filtered_cols[key].filter.set(op, value, value2=value2) + + def set_sort(self, *args): + """Set sort parameters for main query. Resets record cache. + + If keys are passed in that do not belong to this grid, raise user warnings + (not exceptions). These warnings are suppressed if the grid has a "foreign" + session assigned (i.e. two grids share some of the same columns, and should + load as much information as possible from the shared session key). + + Args: + Each arg is expected to be a column key. If the sort is to be descending for + that key, prepend with a "-". + E.g. `grid.set_sort('author', '-post_date')` + """ + self.clear_record_cache(preserve_count=True) + self.order_by = [] + + for key in args: + if not key: + continue + flag_desc = False + if key.startswith('-'): + flag_desc = True + key = key[1:] + if key in self.key_column_map and self.key_column_map[key].can_sort: + self.order_by.append((key, flag_desc)) + elif not self.foreign_session_loaded: + self.user_warnings.append(_('''can't sort on invalid key "{key}"''', key=key)) + + def set_paging(self, per_page, on_page): + """Set paging parameters for the main query. Resets record cache. + + Args: + per_page (int): Record limit for each page. + on_page (int): With `per_page`, computes the offset. + """ + self.clear_record_cache(preserve_count=True) + self.per_page = per_page + self.on_page = on_page + + def clear_record_cache(self, preserve_count=False): + """Reset records and record count cached from previous queries. + + Args: + preserve_count (bool): Direct grid to retain count of records, effectively removing + only the table of records itself. + """ + if not preserve_count: + self._record_count = None + self._records = None + + @property + def ident(self): + return self._ident \ + or self.identifier \ + or case_cw2us(self.__class__.__name__) + + @property + def default_session_key(self): + return f'_{self.__class__.__name__}' + + @property + def has_filters(self): + """Indicates whether filters will be applied in `build_query`. + + Returns: + bool: True if filter(s) have op/value set or single search value is given. + """ + for col in six.itervalues(self.filtered_cols): + if col.filter.is_active: + return True + return self.search_value is not None + + @property + def has_sort(self): + """Indicates whether ordering will be applied in `build_query`. + + Returns: + bool: True if grid's `order_by` list is populated. + """ + return bool(self.order_by) + + @property + def record_count(self): + """Count of records for current filtered query. + + Value is cached to prevent duplicate query execution. Methods changing + the query (e.g. `set_filter`) will reset the cached value. + + Returns: + int: Count of records. + """ + if self._record_count is None: + query = self.build_query(for_count=True) + t0 = time.perf_counter() + self._record_count = query.count() + t1 = time.perf_counter() + log.debug('Count query ran in {} seconds'.format(t1 - t0)) + return self._record_count + + @property + def records(self): + """Records returned for current filtered/sorted/paged query. + + Result is cached to prevent duplicate query execution. Methods changing + the query (e.g. `set_filter`) will reset the cached result. + + Returns: + list(Any): Result records from SQLAlchemy query. + """ + if self._records is None: + query = self.build_query() + t0 = time.perf_counter() + self._records = query.all() + t1 = time.perf_counter() + log.debug('Data query ran in {} seconds'.format(t1 - t0)) + return self._records + + def _totals_col_results(self, page_totals_only): + """Executes query to retrieve subtotals for the filtered query. + + A single result record is returned, which will have fields corresponding to all of the + grid columns (same as a record returned in the general records query). + + Args: + page_totals_only (bool): Tells query builder to use only current page records. + + Returns: + Any: Single result record. + """ + SUB = self.build_query(for_count=(not page_totals_only)).subquery() + + cols = [] + # Not all columns can be totaled. But, we should put in null placeholders + # for any untotaled columns, so that the same query indices from query_base + # can be applied. + # This will apply to any columns with an expr. Other subtotaled columns can be + # tacked onto the end - these will not be indexed and must be referred to by name + for colobj in [ + col for col in self.columns if col.expr is not None + ] + [ + coltuple[1] for _, coltuple in self.subtotal_cols.items() if coltuple[1].expr is None + ]: + colname = colobj._query_key or colobj.key + + if colobj.key not in self.subtotal_cols: + cols.append( + sa.literal(None).label(colname) + ) + continue + + sa_aggregate_func, _ = self.subtotal_cols[colobj.key] + + # column may have a label. If it does, use it + if isinstance(colobj.expr, sasql.expression.Label): + aggregate_this = sasql.text(colobj.key) + elif colobj.expr is None: + aggregate_this = sasql.literal_column(colobj.key) + else: + aggregate_this = colobj.expr + + # sa_aggregate_func could be an expression, or a callable. If it is callable, give it + # the column + labeled_aggregate_col = None + if callable(sa_aggregate_func): + labeled_aggregate_col = sa_aggregate_func(aggregate_this).label(colname) + elif isinstance(sa_aggregate_func, six.string_types): + labeled_aggregate_col = sasql.literal_column(sa_aggregate_func).label(colname) + else: + labeled_aggregate_col = sa_aggregate_func.label(colname) + cols.append(labeled_aggregate_col) + cols.append(sa.literal(1).label('__is_total__')) + + t0 = time.perf_counter() + query = self.manager.sa_query(*cols) + # WARN: probably not future proof + query._set_select_from([SUB], True) + result = query.first() + t1 = time.perf_counter() + log.debug('Totals query ran in {} seconds'.format(t1 - t0)) + + return result + + @property + def page_totals(self): + """Executes query to retrieve subtotals for the filtered query on the current page. + + For page totals to be queried/returned, the grid's `subtotals` must be page/all + and one or more columns must have subtotals configured. + + A single result record is returned, which will have fields corresponding to all of the + grid columns (same as a record returned in the general records query). + + Returns: + Any: Single result record, or None if page totals are not configured. + """ + if ( + self._page_totals is None + and self.subtotals in ('page', 'all') + and self.subtotal_cols + ): + self._page_totals = self._totals_col_results(page_totals_only=True) + return self._page_totals + + @property + def grand_totals(self): + """Executes query to retrieve subtotals for the filtered query. + + For grand totals to be queried/returned, the grid's `subtotals` must be grand/all + and one or more columns must have subtotals configured. + + A single result record is returned, which will have fields corresponding to all of the + grid columns (same as a record returned in the general records query). + + Returns: + Any: Single result record, or None if grand totals are not configured. + """ + if ( + self._grand_totals is None + and self.subtotals in ('grand', 'all') + and self.subtotal_cols + ): + self._grand_totals = self._totals_col_results(page_totals_only=False) + return self._grand_totals + + @property + def page_count(self): + """Page count, or 1 if no `per_page` is set. + """ + if self.per_page is None: + return 1 + return max(0, self.record_count - 1) // self.per_page + 1 + + def build_query(self, for_count=False): + """Constructs, but does not execute, a grid query from columns and configuration. + + This is the query the grid functions trust for results for records, count, page + count, etc. Customization of the query should happen here or in the methods called + within. + + Build sequence: + - `query_base` + - `query_prep` + - `query_filters` + - `query_sort` + - `query_paging` + + Args: + for_count (bool, optional): Excludes sort/page from query. Defaults to False. + + Returns: + Query: SQLAlchemy query object + """ + log.debug(str(self)) + + has_filters = self.has_filters + query = self.query_base(self.has_sort, has_filters) + query = self.query_prep(query, self.has_sort or for_count, has_filters) + + if has_filters: + query = self.query_filters(query) + else: + log.debug('No filters') + + if for_count: + return query + + query = self.query_sort(query) + if self.pager_on: + query = self.query_paging(query) + + return query + + def set_records(self, records): + """Assign a set of records to the grid's cache. + + Useful for simple grids that simply need to be rendered as a table. Note that any + ops performed on the grid, such as setting filter/sort/page options, will clear this + cached information. + + Args: + records (list(Any)): List of record objects that can be referenced for column data. + """ + self._record_count = len(records) + self._records = records + + def query_base(self, has_sort, has_filters): + """Construct a query from grid columns, using grid's join/filter/sort attributes. + + Used by `build_query` to establish the basic query from column spec. If query is to be + modified, it is recommended to do so in `query_prep` if possible, rather than overriding + `query_base`. + + Args: + has_sort (bool): Tells method not to order query, since the grid has sort params. + has_filters (bool): Tells method if grid has filter params. Not used. + + Returns: + Query: SQLAlchemy query + """ + for idx, column in enumerate(filter(lambda col: col.expr is not None, self.columns)): + column._query_idx = idx + cols = [col.expr for col in self.columns if col.expr is not None] + query = self.manager.sa_query(*cols) + + if self.query_select_from is not None: + query = query.select_from(*tolist(self.query_select_from)) + + for join_terms in (tolist(self.query_joins) or tuple()): + query = query.join(*tolist(join_terms)) + + for join_terms in (tolist(self.query_outer_joins) or tuple()): + query = query.outerjoin(*tolist(join_terms)) + + if self.query_filter: + query = query.filter(*tolist(self.query_filter)) + + if not has_sort and self.query_default_sort is not None: + query = query.order_by(*tolist(self.query_default_sort)) + + return query + + def query_prep(self, query, has_sort, has_filters): + """Modify the query that was constructed in `query_base`. + + Joins, query filtering, and default sorting can be applied via grid attributes. However, + sometimes grid queries need columns added, instance-time modifications applied, etc. + + Called by `build_query`. + + Args: + query (Query): SQLAlchemy query object. + has_sort (bool): Tells method grid has sort params defined. + has_filters (bool): Tells method if grid has filter params. + + Returns: + Query: SQLAlchemy query + """ + return query + + def query_filters(self, query): + """Modify the query by applying filter terms. + + Called by `build_query` to apply any column filters as needed. Also enacts + the single-search value if specified. + + Args: + query (Query): SQLAlchemy query object. + + Returns: + Query: SQLAlchemy query + """ + filter_display = [] + if self.search_value: + query = self.apply_search(query, self.search_value) + + for col in six.itervalues(self.filtered_cols): + if col.filter.is_active: + filter_display.append('{}: {}'.format(col.key, str(col.filter))) + query = col.filter.apply(query) + if filter_display: + log.debug(';'.join(filter_display)) + else: + log.debug('No filters') + return query + + def apply_search(self, query, value): + """Modify the query by applying a filter term constructed from search clauses. + + Calls each filter search expression factory with the search value to get a search + clause, then ORs them all together for the main query. + + Args: + query (Query): SQLAlchemy query. + value (str): Search value. + + Returns: + Query: SQLAlchemy query + """ + filter_method = query.having if self.search_uses_aggregate else query.filter + filter_clauses = list(filter( + lambda item: item is not None, + (expr(value) for expr in self.search_expression_generators) + )) + + if not filter_clauses: + return query + return filter_method(sa.or_(*filter_clauses)) + + def query_paging(self, query): + """Modify the query by applying limit/offset to match grid parameters. + + Args: + query (Query): SQLAlchemy query. + + Returns: + Query: SQLAlchemy query + """ + if self.on_page and self.per_page: + offset = (self.on_page - 1) * self.per_page + query = query.offset(offset).limit(self.per_page) + log.debug('Page {}; {} per page'.format(self.on_page, self.per_page)) + return query + + def query_sort(self, query): + """Modify the query by applying sort to match grid parameters. + + Args: + query (Query): SQLAlchemy query. + + Returns: + Query: SQLAlchemy query + """ + redundant = [] + sort_display = [] + for key, flag_desc in self.order_by: + if key in self.key_column_map: + col = self.key_column_map[key] + # remove any redundant names, whichever comes first is what we will keep + if col.key in redundant: + continue + else: + sort_display.append(col.key) + redundant.append(col.key) + query = col.apply_sort(query, flag_desc) + if sort_display: + log.debug(','.join(sort_display)) + else: + log.debug('No sorts') + + # Special consideration for MSSQL, because if paging is to work, the query + # must have an ORDER BY clause. This is problematic, because if an app + # does not test for the query case where paging is enabled for page > 1, + # the query will not hit the error state. Fix the case if possible. + if ( + self.pager_on + and self.manager + and self.manager.db.engine.dialect.name == 'mssql' + and not query._order_by_clauses + ): + query = self._fix_mssql_order_by(query) + + return query + + def _fix_mssql_order_by(self, query): + """MSSQL must have an ORDER BY for paging to work. If no sort clause has been + defined, sort by the first column. If that doesn't work, error out. + """ + if len(self.columns): + query = self.columns[0].apply_sort(query, False) + if query._order_by_clauses: + return query + raise Exception( + 'Paging is enabled, but query does not have ORDER BY clause required for MSSQL' + ) + + def build_qs_args(self, include_session=False): + """Build a URL query string based on current grid attributes. + + This is designed to be framework-agnostic and not require a request context. Usually + the result would be used in a background task or similar (i.e. outside the flow of the + rendered grid), so typically the session key is unnecessary. + + Args: + include_session (bool, optional): Include session_key in the string. Defaults to False. + """ + return QueryStringBuilder(self, include_session)() + + def apply_qs_args(self, add_user_warnings=True, grid_args=None): + """Process args from manager for filter/page/sort/export. + + Args: + add_user_warnings (bool, optional): Add flash messages for warnings. Defaults to True. + grid_args (MultiDict, optional): Supply args directly to the grid. + """ + args = grid_args if grid_args is not None else self.manager.get_args(self) + + if self.session_on: + self.session_key = args.get('session_key') or self.session_key + self.foreign_session_loaded = args.get('__foreign_session_loaded__', False) + + # search + self._apply_search(args) + + # filtering (make sure this is above paging otherwise self.page_count + # used in the paging section below won't work) + self._apply_filtering(args) + + # paging + self._apply_paging(args) + + # sorting + self._apply_sorting(args) + + # export + self._apply_export(args) + + # Having this here is not ideal. Due to separation of concerns, it would be nice to + # have flash warnings in the HTML renderer. However, by the time the renderer is + # called, an app template has probably already loaded and rendered any messages to + # be shown, and it's too late to add new ones. + if add_user_warnings: + for msg in self.user_warnings: + self.manager.flash_message('warning', msg) + + def _apply_search(self, args): + if ( + 'search' in args + and self.can_search() + ): + self.search_value = args['search'].strip() if args['search'] else None + + def _apply_filtering(self, args): + """Turn request/session args into filter settings. + + Args: + args (MultiDict): Full arguments to search for filters. + """ + for col in six.itervalues(self.filtered_cols): + filter = col.filter + filter_op_qsk = 'op({0})'.format(col.key) + filter_v1_qsk = 'v1({0})'.format(col.key) + filter_v2_qsk = 'v2({0})'.format(col.key) + + filter_op_value = args.get(filter_op_qsk, None) + + if filter._default_op: + filter.set(None, None, None) + + if filter_op_value is not None: + if filter.receives_list: + v1 = args.getlist(filter_v1_qsk) + v2 = args.getlist(filter_v2_qsk) + else: + v1 = args.get(filter_v1_qsk, None) + v2 = args.get(filter_v2_qsk, None) + + try: + filter.set( + filter_op_value, + v1, + v2, + ) + except validators.ValueInvalid as e: + invalid_msg = filter.format_invalid(e, col) + self.user_warnings.append(invalid_msg) + + def _apply_paging(self, args): + """Turn request/session args into page settings. + + Args: + args (MultiDict): Full arguments to search for paging. + """ + pp_qsk = 'perpage' + if pp_qsk in args: + per_page = self.apply_validator(validators.IntValidator, args[pp_qsk], pp_qsk) + if per_page is None: + per_page = self.__class__.per_page + elif per_page < 1: + per_page = 1 + self.per_page = per_page + + op_qsk = 'onpage' + if op_qsk in args: + on_page = self.apply_validator(validators.IntValidator, args[op_qsk], op_qsk) + if on_page is None or on_page < 1: + on_page = 1 + if on_page > self.page_count: + on_page = self.page_count + self.on_page = on_page + + def _apply_sorting(self, args): + """Turn request/session args into sort settings. + + No limit is present here for how many sort args may be passed. However, the + args are expected to be contiguous. I.e. sort1, sort2, sort3 will be processed, + but sort1, sort3, sort5 will only process sort1. + + Args: + args (MultiDict): Full arguments to search for sort keys. + """ + counter = 0 + sort_qs_values = [] + + while True: + counter += 1 + sort_arg = 'sort{}'.format(counter) + if sort_arg not in args: + break + sort_qs_values.append(args[sort_arg]) + + if sort_qs_values: + self.set_sort(*sort_qs_values) + + def _apply_export(self, args): + # handle other file formats + self.set_export_to(args.get('export_to', None)) + + def prefix_qs_arg_key(self, key): + """Given a bare arg key, return the prefixed version that will actually be in the request. + + This is necessary for render targets that will construct ensuing requests. Prefixing is + not needed for incoming args on internal grid ops, as long as the grid manager's + args loaders sanitize the args properly. + + Args: + key (str): Bare arg key. + + Returns: + str: Prefixed arg key. + """ + return '{0}{1}'.format(self.qs_prefix, key) + + def apply_validator(self, validator, value, qs_arg_key): + """Apply a webgrid validator to value, and produce a warning if invalid. + + Args: + validator (Validator): webgrid validator. + value (str): Value to validate. + qs_arg_key (str): Arg name to include in warning if value is invalid. + + Returns: + Any: Output of `validator.to_python(value)`, or `None` if invalid. + """ + try: + return validator().process(value) + except validators.ValueInvalid: + invalid_msg = _('"{arg}" grid argument invalid, ignoring', arg=qs_arg_key) + self.user_warnings.append(invalid_msg) + return None + + def set_export_to(self, to): + """Set export parameter after validating it exists in known targets. + + Args: + to (str): Renderer attribute if it is known. Invalid value ignored. + """ + if to in self.allowed_export_targets: + self.export_to = to + + def export_as_response(self, wb=None, sheet_name=None): + """Return renderer response for view layer to provide as a file. + + Args: + wb (Workbook, optional): XlsxWriter Workbook. Defaults to None. + sheet_name (Worksheet, optional): XlsxWriter Worksheet. Defaults to None. + + Raises: + ValueError: No export parameter given. + + Returns: + Response: Return response processed through renderer and manager. + """ + if not self.export_to: + raise ValueError('No export format set') + exporter = getattr(self, self.export_to) + if self.export_to == 'xlsx': + return exporter.as_response(wb, sheet_name) + return exporter.as_response() + + def __repr__(self): + return ''.format(self.__class__.__name__) + + +def row_styler(f): + f.__grid_rowstyler__ = True + return f + + +def col_styler(for_column): + def decorator(f): + f.__grid_colstyler__ = for_column + return f + return decorator + + +def col_filter(for_column): + def decorator(f): + f.__grid_colfilter__ = for_column + return f + return decorator diff --git a/webgrid/blazeweb.py b/src/webgrid/blazeweb.py similarity index 100% rename from webgrid/blazeweb.py rename to src/webgrid/blazeweb.py diff --git a/src/webgrid/cli.py b/src/webgrid/cli.py deleted file mode 100644 index 19a5483..0000000 --- a/src/webgrid/cli.py +++ /dev/null @@ -1,2 +0,0 @@ -def main(): - print('Hello from', __name__) diff --git a/webgrid/extensions.py b/src/webgrid/extensions.py similarity index 100% rename from webgrid/extensions.py rename to src/webgrid/extensions.py diff --git a/webgrid/filters.py b/src/webgrid/filters.py similarity index 100% rename from webgrid/filters.py rename to src/webgrid/filters.py diff --git a/webgrid/flask.py b/src/webgrid/flask.py similarity index 100% rename from webgrid/flask.py rename to src/webgrid/flask.py diff --git a/webgrid/i18n/babel.cfg b/src/webgrid/i18n/babel.cfg similarity index 100% rename from webgrid/i18n/babel.cfg rename to src/webgrid/i18n/babel.cfg diff --git a/webgrid/i18n/es/LC_MESSAGES/webgrid.mo b/src/webgrid/i18n/es/LC_MESSAGES/webgrid.mo similarity index 100% rename from webgrid/i18n/es/LC_MESSAGES/webgrid.mo rename to src/webgrid/i18n/es/LC_MESSAGES/webgrid.mo diff --git a/webgrid/i18n/es/LC_MESSAGES/webgrid.po b/src/webgrid/i18n/es/LC_MESSAGES/webgrid.po similarity index 100% rename from webgrid/i18n/es/LC_MESSAGES/webgrid.po rename to src/webgrid/i18n/es/LC_MESSAGES/webgrid.po diff --git a/webgrid/i18n/webgrid.pot b/src/webgrid/i18n/webgrid.pot similarity index 100% rename from webgrid/i18n/webgrid.pot rename to src/webgrid/i18n/webgrid.pot diff --git a/webgrid/renderers.py b/src/webgrid/renderers.py similarity index 100% rename from webgrid/renderers.py rename to src/webgrid/renderers.py diff --git a/webgrid/static/application_form_edit.png b/src/webgrid/static/application_form_edit.png similarity index 100% rename from webgrid/static/application_form_edit.png rename to src/webgrid/static/application_form_edit.png diff --git a/webgrid/static/b_firstpage.png b/src/webgrid/static/b_firstpage.png similarity index 100% rename from webgrid/static/b_firstpage.png rename to src/webgrid/static/b_firstpage.png diff --git a/webgrid/static/b_lastpage.png b/src/webgrid/static/b_lastpage.png similarity index 100% rename from webgrid/static/b_lastpage.png rename to src/webgrid/static/b_lastpage.png diff --git a/webgrid/static/b_nextpage.png b/src/webgrid/static/b_nextpage.png similarity index 100% rename from webgrid/static/b_nextpage.png rename to src/webgrid/static/b_nextpage.png diff --git a/webgrid/static/b_prevpage.png b/src/webgrid/static/b_prevpage.png similarity index 100% rename from webgrid/static/b_prevpage.png rename to src/webgrid/static/b_prevpage.png diff --git a/webgrid/static/bd_firstpage.png b/src/webgrid/static/bd_firstpage.png similarity index 100% rename from webgrid/static/bd_firstpage.png rename to src/webgrid/static/bd_firstpage.png diff --git a/webgrid/static/bd_lastpage.png b/src/webgrid/static/bd_lastpage.png similarity index 100% rename from webgrid/static/bd_lastpage.png rename to src/webgrid/static/bd_lastpage.png diff --git a/webgrid/static/bd_nextpage.png b/src/webgrid/static/bd_nextpage.png similarity index 100% rename from webgrid/static/bd_nextpage.png rename to src/webgrid/static/bd_nextpage.png diff --git a/webgrid/static/bd_prevpage.png b/src/webgrid/static/bd_prevpage.png similarity index 100% rename from webgrid/static/bd_prevpage.png rename to src/webgrid/static/bd_prevpage.png diff --git a/webgrid/static/delete.png b/src/webgrid/static/delete.png similarity index 100% rename from webgrid/static/delete.png rename to src/webgrid/static/delete.png diff --git a/webgrid/static/gettext.min.js b/src/webgrid/static/gettext.min.js similarity index 100% rename from webgrid/static/gettext.min.js rename to src/webgrid/static/gettext.min.js diff --git a/webgrid/static/i18n/es/LC_MESSAGES/webgrid.json b/src/webgrid/static/i18n/es/LC_MESSAGES/webgrid.json similarity index 100% rename from webgrid/static/i18n/es/LC_MESSAGES/webgrid.json rename to src/webgrid/static/i18n/es/LC_MESSAGES/webgrid.json diff --git a/webgrid/static/jquery.multiple.select.js b/src/webgrid/static/jquery.multiple.select.js similarity index 100% rename from webgrid/static/jquery.multiple.select.js rename to src/webgrid/static/jquery.multiple.select.js diff --git a/webgrid/static/multiple-select.css b/src/webgrid/static/multiple-select.css similarity index 100% rename from webgrid/static/multiple-select.css rename to src/webgrid/static/multiple-select.css diff --git a/webgrid/static/multiple-select.png b/src/webgrid/static/multiple-select.png similarity index 100% rename from webgrid/static/multiple-select.png rename to src/webgrid/static/multiple-select.png diff --git a/webgrid/static/th_arrow_down.png b/src/webgrid/static/th_arrow_down.png similarity index 100% rename from webgrid/static/th_arrow_down.png rename to src/webgrid/static/th_arrow_down.png diff --git a/webgrid/static/th_arrow_up.png b/src/webgrid/static/th_arrow_up.png similarity index 100% rename from webgrid/static/th_arrow_up.png rename to src/webgrid/static/th_arrow_up.png diff --git a/webgrid/static/webgrid.css b/src/webgrid/static/webgrid.css similarity index 100% rename from webgrid/static/webgrid.css rename to src/webgrid/static/webgrid.css diff --git a/webgrid/static/webgrid.js b/src/webgrid/static/webgrid.js similarity index 100% rename from webgrid/static/webgrid.js rename to src/webgrid/static/webgrid.js diff --git a/webgrid/templates/grid.html b/src/webgrid/templates/grid.html similarity index 100% rename from webgrid/templates/grid.html rename to src/webgrid/templates/grid.html diff --git a/webgrid/templates/grid_footer.html b/src/webgrid/templates/grid_footer.html similarity index 100% rename from webgrid/templates/grid_footer.html rename to src/webgrid/templates/grid_footer.html diff --git a/webgrid/templates/grid_header.html b/src/webgrid/templates/grid_header.html similarity index 100% rename from webgrid/templates/grid_header.html rename to src/webgrid/templates/grid_header.html diff --git a/webgrid/templates/grid_table.html b/src/webgrid/templates/grid_table.html similarity index 100% rename from webgrid/templates/grid_table.html rename to src/webgrid/templates/grid_table.html diff --git a/webgrid/templates/header_filtering.html b/src/webgrid/templates/header_filtering.html similarity index 100% rename from webgrid/templates/header_filtering.html rename to src/webgrid/templates/header_filtering.html diff --git a/webgrid/templates/header_paging.html b/src/webgrid/templates/header_paging.html similarity index 100% rename from webgrid/templates/header_paging.html rename to src/webgrid/templates/header_paging.html diff --git a/webgrid/templates/header_sorting.html b/src/webgrid/templates/header_sorting.html similarity index 100% rename from webgrid/templates/header_sorting.html rename to src/webgrid/templates/header_sorting.html diff --git a/webgrid/testing.py b/src/webgrid/testing.py similarity index 100% rename from webgrid/testing.py rename to src/webgrid/testing.py diff --git a/webgrid/types.py b/src/webgrid/types.py similarity index 100% rename from webgrid/types.py rename to src/webgrid/types.py diff --git a/webgrid/utils.py b/src/webgrid/utils.py similarity index 100% rename from webgrid/utils.py rename to src/webgrid/utils.py diff --git a/webgrid/validators.py b/src/webgrid/validators.py similarity index 100% rename from webgrid/validators.py rename to src/webgrid/validators.py diff --git a/src/webgrid/version.py b/src/webgrid/version.py index 1f6518e..1ff6ff2 100644 --- a/src/webgrid/version.py +++ b/src/webgrid/version.py @@ -1 +1 @@ -VERSION = '0.1.0' +VERSION = '0.5.10' diff --git a/webgrid/tests/__init__.py b/src/webgrid_blazeweb_ta/__init__.py similarity index 100% rename from webgrid/tests/__init__.py rename to src/webgrid_blazeweb_ta/__init__.py diff --git a/webgrid_blazeweb_ta/application.py b/src/webgrid_blazeweb_ta/application.py similarity index 100% rename from webgrid_blazeweb_ta/application.py rename to src/webgrid_blazeweb_ta/application.py diff --git a/webgrid_blazeweb_ta/config/__init__.py b/src/webgrid_blazeweb_ta/config/__init__.py similarity index 100% rename from webgrid_blazeweb_ta/config/__init__.py rename to src/webgrid_blazeweb_ta/config/__init__.py diff --git a/webgrid_blazeweb_ta/config/settings.py b/src/webgrid_blazeweb_ta/config/settings.py similarity index 100% rename from webgrid_blazeweb_ta/config/settings.py rename to src/webgrid_blazeweb_ta/config/settings.py diff --git a/webgrid_blazeweb_ta/config/site_settings.py_tpl b/src/webgrid_blazeweb_ta/config/site_settings.py_tpl similarity index 100% rename from webgrid_blazeweb_ta/config/site_settings.py_tpl rename to src/webgrid_blazeweb_ta/config/site_settings.py_tpl diff --git a/webgrid_blazeweb_ta/extensions.py b/src/webgrid_blazeweb_ta/extensions.py similarity index 100% rename from webgrid_blazeweb_ta/extensions.py rename to src/webgrid_blazeweb_ta/extensions.py diff --git a/webgrid_blazeweb_ta/i18n/babel.cfg b/src/webgrid_blazeweb_ta/i18n/babel.cfg similarity index 100% rename from webgrid_blazeweb_ta/i18n/babel.cfg rename to src/webgrid_blazeweb_ta/i18n/babel.cfg diff --git a/webgrid_blazeweb_ta/i18n/es/LC_MESSAGES/webgrid_blazeweb_ta.mo b/src/webgrid_blazeweb_ta/i18n/es/LC_MESSAGES/webgrid_blazeweb_ta.mo similarity index 100% rename from webgrid_blazeweb_ta/i18n/es/LC_MESSAGES/webgrid_blazeweb_ta.mo rename to src/webgrid_blazeweb_ta/i18n/es/LC_MESSAGES/webgrid_blazeweb_ta.mo diff --git a/webgrid_blazeweb_ta/i18n/es/LC_MESSAGES/webgrid_blazeweb_ta.po b/src/webgrid_blazeweb_ta/i18n/es/LC_MESSAGES/webgrid_blazeweb_ta.po similarity index 100% rename from webgrid_blazeweb_ta/i18n/es/LC_MESSAGES/webgrid_blazeweb_ta.po rename to src/webgrid_blazeweb_ta/i18n/es/LC_MESSAGES/webgrid_blazeweb_ta.po diff --git a/webgrid_blazeweb_ta/i18n/webgrid_blazeweb_ta.pot b/src/webgrid_blazeweb_ta/i18n/webgrid_blazeweb_ta.pot similarity index 100% rename from webgrid_blazeweb_ta/i18n/webgrid_blazeweb_ta.pot rename to src/webgrid_blazeweb_ta/i18n/webgrid_blazeweb_ta.pot diff --git a/webgrid_blazeweb_ta/__init__.py b/src/webgrid_blazeweb_ta/model/__init__.py similarity index 100% rename from webgrid_blazeweb_ta/__init__.py rename to src/webgrid_blazeweb_ta/model/__init__.py diff --git a/webgrid_blazeweb_ta/model/orm.py b/src/webgrid_blazeweb_ta/model/orm.py similarity index 100% rename from webgrid_blazeweb_ta/model/orm.py rename to src/webgrid_blazeweb_ta/model/orm.py diff --git a/webgrid_blazeweb_ta/model/__init__.py b/src/webgrid_blazeweb_ta/tasks/__init__.py similarity index 100% rename from webgrid_blazeweb_ta/model/__init__.py rename to src/webgrid_blazeweb_ta/tasks/__init__.py diff --git a/webgrid_blazeweb_ta/tasks/init_db.py b/src/webgrid_blazeweb_ta/tasks/init_db.py similarity index 100% rename from webgrid_blazeweb_ta/tasks/init_db.py rename to src/webgrid_blazeweb_ta/tasks/init_db.py diff --git a/webgrid_blazeweb_ta/templates/manage_people.html b/src/webgrid_blazeweb_ta/templates/manage_people.html similarity index 100% rename from webgrid_blazeweb_ta/templates/manage_people.html rename to src/webgrid_blazeweb_ta/templates/manage_people.html diff --git a/webgrid_blazeweb_ta/templates/nonstandard_header.css b/src/webgrid_blazeweb_ta/templates/nonstandard_header.css similarity index 100% rename from webgrid_blazeweb_ta/templates/nonstandard_header.css rename to src/webgrid_blazeweb_ta/templates/nonstandard_header.css diff --git a/webgrid_blazeweb_ta/templates/nonstandard_header.html b/src/webgrid_blazeweb_ta/templates/nonstandard_header.html similarity index 100% rename from webgrid_blazeweb_ta/templates/nonstandard_header.html rename to src/webgrid_blazeweb_ta/templates/nonstandard_header.html diff --git a/webgrid_blazeweb_ta/tasks/__init__.py b/src/webgrid_blazeweb_ta/tests/__init__.py similarity index 100% rename from webgrid_blazeweb_ta/tasks/__init__.py rename to src/webgrid_blazeweb_ta/tests/__init__.py diff --git a/webgrid_blazeweb_ta/tests/grids.py b/src/webgrid_blazeweb_ta/tests/grids.py similarity index 100% rename from webgrid_blazeweb_ta/tests/grids.py rename to src/webgrid_blazeweb_ta/tests/grids.py diff --git a/webgrid_blazeweb_ta/views.py b/src/webgrid_blazeweb_ta/views.py similarity index 100% rename from webgrid_blazeweb_ta/views.py rename to src/webgrid_blazeweb_ta/views.py diff --git a/webgrid_blazeweb_ta/tests/__init__.py b/src/webgrid_ta/__init__.py similarity index 100% rename from webgrid_blazeweb_ta/tests/__init__.py rename to src/webgrid_ta/__init__.py diff --git a/webgrid_ta/app.py b/src/webgrid_ta/app.py similarity index 100% rename from webgrid_ta/app.py rename to src/webgrid_ta/app.py diff --git a/webgrid_ta/data/basic_table.html b/src/webgrid_ta/data/basic_table.html similarity index 100% rename from webgrid_ta/data/basic_table.html rename to src/webgrid_ta/data/basic_table.html diff --git a/webgrid_ta/data/people_table.html b/src/webgrid_ta/data/people_table.html similarity index 100% rename from webgrid_ta/data/people_table.html rename to src/webgrid_ta/data/people_table.html diff --git a/webgrid_ta/extensions.py b/src/webgrid_ta/extensions.py similarity index 100% rename from webgrid_ta/extensions.py rename to src/webgrid_ta/extensions.py diff --git a/webgrid_ta/grids.py b/src/webgrid_ta/grids.py similarity index 100% rename from webgrid_ta/grids.py rename to src/webgrid_ta/grids.py diff --git a/webgrid_ta/helpers.py b/src/webgrid_ta/helpers.py similarity index 100% rename from webgrid_ta/helpers.py rename to src/webgrid_ta/helpers.py diff --git a/webgrid_ta/i18n/babel.cfg b/src/webgrid_ta/i18n/babel.cfg similarity index 100% rename from webgrid_ta/i18n/babel.cfg rename to src/webgrid_ta/i18n/babel.cfg diff --git a/webgrid_ta/i18n/es/LC_MESSAGES/webgrid_ta.mo b/src/webgrid_ta/i18n/es/LC_MESSAGES/webgrid_ta.mo similarity index 100% rename from webgrid_ta/i18n/es/LC_MESSAGES/webgrid_ta.mo rename to src/webgrid_ta/i18n/es/LC_MESSAGES/webgrid_ta.mo diff --git a/webgrid_ta/i18n/es/LC_MESSAGES/webgrid_ta.po b/src/webgrid_ta/i18n/es/LC_MESSAGES/webgrid_ta.po similarity index 100% rename from webgrid_ta/i18n/es/LC_MESSAGES/webgrid_ta.po rename to src/webgrid_ta/i18n/es/LC_MESSAGES/webgrid_ta.po diff --git a/webgrid_ta/i18n/webgrid_ta.pot b/src/webgrid_ta/i18n/webgrid_ta.pot similarity index 100% rename from webgrid_ta/i18n/webgrid_ta.pot rename to src/webgrid_ta/i18n/webgrid_ta.pot diff --git a/webgrid_ta/manage.py b/src/webgrid_ta/manage.py similarity index 100% rename from webgrid_ta/manage.py rename to src/webgrid_ta/manage.py diff --git a/webgrid_ta/model/__init__.py b/src/webgrid_ta/model/__init__.py similarity index 100% rename from webgrid_ta/model/__init__.py rename to src/webgrid_ta/model/__init__.py diff --git a/webgrid_ta/model/entities.py b/src/webgrid_ta/model/entities.py similarity index 100% rename from webgrid_ta/model/entities.py rename to src/webgrid_ta/model/entities.py diff --git a/webgrid_ta/model/helpers.py b/src/webgrid_ta/model/helpers.py similarity index 100% rename from webgrid_ta/model/helpers.py rename to src/webgrid_ta/model/helpers.py diff --git a/webgrid_ta/templates/groups.html b/src/webgrid_ta/templates/groups.html similarity index 100% rename from webgrid_ta/templates/groups.html rename to src/webgrid_ta/templates/groups.html diff --git a/webgrid_ta/templates/index.html b/src/webgrid_ta/templates/index.html similarity index 100% rename from webgrid_ta/templates/index.html rename to src/webgrid_ta/templates/index.html diff --git a/webgrid_ta/views.py b/src/webgrid_ta/views.py similarity index 100% rename from webgrid_ta/views.py rename to src/webgrid_ta/views.py diff --git a/webgrid_ta/__init__.py b/src/webgrid_tests/__init__.py similarity index 100% rename from webgrid_ta/__init__.py rename to src/webgrid_tests/__init__.py diff --git a/webgrid/tests/conftest.py b/src/webgrid_tests/conftest.py similarity index 100% rename from webgrid/tests/conftest.py rename to src/webgrid_tests/conftest.py diff --git a/webgrid/tests/data/basic_table.html b/src/webgrid_tests/data/basic_table.html similarity index 100% rename from webgrid/tests/data/basic_table.html rename to src/webgrid_tests/data/basic_table.html diff --git a/webgrid/tests/data/people_table.html b/src/webgrid_tests/data/people_table.html similarity index 100% rename from webgrid/tests/data/people_table.html rename to src/webgrid_tests/data/people_table.html diff --git a/webgrid/tests/data/stopwatch_table.html b/src/webgrid_tests/data/stopwatch_table.html similarity index 100% rename from webgrid/tests/data/stopwatch_table.html rename to src/webgrid_tests/data/stopwatch_table.html diff --git a/webgrid/tests/helpers.py b/src/webgrid_tests/helpers.py similarity index 100% rename from webgrid/tests/helpers.py rename to src/webgrid_tests/helpers.py diff --git a/webgrid/tests/test_api.py b/src/webgrid_tests/test_api.py similarity index 100% rename from webgrid/tests/test_api.py rename to src/webgrid_tests/test_api.py diff --git a/src/webgrid_tests/test_cli.py b/src/webgrid_tests/test_cli.py deleted file mode 100644 index 60bfadc..0000000 --- a/src/webgrid_tests/test_cli.py +++ /dev/null @@ -1,5 +0,0 @@ -from webgrid.cli import main - - -def test_main(): - main() diff --git a/webgrid/tests/test_columns.py b/src/webgrid_tests/test_columns.py similarity index 100% rename from webgrid/tests/test_columns.py rename to src/webgrid_tests/test_columns.py diff --git a/webgrid/tests/test_filters.py b/src/webgrid_tests/test_filters.py similarity index 100% rename from webgrid/tests/test_filters.py rename to src/webgrid_tests/test_filters.py diff --git a/webgrid/tests/test_rendering.py b/src/webgrid_tests/test_rendering.py similarity index 100% rename from webgrid/tests/test_rendering.py rename to src/webgrid_tests/test_rendering.py diff --git a/webgrid/tests/test_testing.py b/src/webgrid_tests/test_testing.py similarity index 100% rename from webgrid/tests/test_testing.py rename to src/webgrid_tests/test_testing.py diff --git a/webgrid/tests/test_types.py b/src/webgrid_tests/test_types.py similarity index 100% rename from webgrid/tests/test_types.py rename to src/webgrid_tests/test_types.py diff --git a/webgrid/tests/test_unit.py b/src/webgrid_tests/test_unit.py similarity index 100% rename from webgrid/tests/test_unit.py rename to src/webgrid_tests/test_unit.py diff --git a/webgrid/__init__.py b/webgrid/__init__.py deleted file mode 100644 index 60ef4c8..0000000 --- a/webgrid/__init__.py +++ /dev/null @@ -1,1899 +0,0 @@ -from __future__ import absolute_import -import datetime as dt -import inspect -import logging -import sys -import six -import time -import urllib.parse - -from blazeutils.containers import HTMLAttributes -from blazeutils.datastructures import BlankObject, OrderedDict -from blazeutils.helpers import tolist -from blazeutils.numbers import decimalfmt -from blazeutils.strings import case_cw2us, randchars -from blazeutils.spreadsheets import xlsxwriter, openpyxl -import sqlalchemy as sa -import sqlalchemy.sql as sasql - -from . import validators -from .extensions import gettext as _ -from .renderers import HTML, XLSX -from .version import VERSION as __version__ # noqa: F401 - -# conditional imports to support libs without requiring them -try: - import arrow -except ImportError: - arrow = None - -log = logging.getLogger(__name__) - - -# subtotals functions -sum_ = sasql.functions.sum -avg_ = sasql.func.avg - - -def subtotal_function_map(v): - """Maps string value to a function, or passes the value through. - - Recognizes True, "sum" or "avg". If True, "sum" is used as the default - subtotal function. - - Args: - v (Union(str, callable)): Value defining the subtotal method. - - Returns: - Union(str, callable): `sum` or `avg` SQLAlchemy functions, or the value. - """ - if v is True or v == 'sum': - return sum_ - elif v == 'avg': - return avg_ - return v - - -class _None(object): - """ - A sentinal object to indicate no value - """ - pass - - -class ExtractionError(TypeError): - """ raised when we are unable to extract a value from the record """ - pass - - -class _DeclarativeMeta(type): - - def __new__(cls, name, bases, class_dict): - class_dict['_rowstylers'] = [] - class_dict['_colstylers'] = [] - class_dict['_colfilters'] = [] - class_columns = [] - - # add columns from base classes - for base in bases: - base_columns = getattr(base, '__cls_cols__', ()) - class_columns.extend(base_columns) - class_columns.extend(class_dict.get('__cls_cols__', ())) - class_dict['__cls_cols__'] = class_columns - - # we have to assign the attribute name - for k, v in six.iteritems(class_dict): - # catalog the row stylers - if getattr(v, '__grid_rowstyler__', None): - class_dict['_rowstylers'].append(v) - - # catalog the column stylers - for_column = getattr(v, '__grid_colstyler__', None) - if for_column: - class_dict['_colstylers'].append((v, for_column)) - - # catalog the column filters - for_column = getattr(v, '__grid_colfilter__', None) - if for_column: - class_dict['_colfilters'].append((v, for_column)) - - return super(_DeclarativeMeta, cls).__new__(cls, name, bases, class_dict) - - -class Column(object): - """Column represents the data and render specification for a table column. - - Args: - label (str): Label to use for filter/sort selection and table header. - - key (Union[Expression, str], optional): Field key or SQLAlchemy expression. - If an expression is provided, column attempts to derive a string key name from - the expression. Defaults to None. - - filter (FilterBase, optional): Filter class or instance. Defaults to None. - - can_sort (bool, optional): Enables column for selection in sort keys. Defaults to True. - - xls_num_format (str, optional): XLSX number/date format. Defaults to None. - - render_in (Union(list(str), callable), optional): Targets to render as a column. - Defaults to _None. - - has_subtotal (Union(bool,str,callable), optional): Subtotal method to use, if any. - True or "sum" will yield a sum total. "avg" maps to average. Can also be a - callable that will be called with the aggregate expression and is expected - to return a SQLAlchemy expression. Defaults to False. - - visible (Union(bool, callable), optional): Enables any target in `render_in`. - Defaults to True. - - group (ColumnGroup, optional): Render grouping under a single heading. Defaults to None. - - Class Attributes: - xls_width (float, optional): Override to autocalculated width in Excel exports. - - xls_num_format (str, optional): Default numeric/date format type. - """ - xls_width = None - xls_num_format = None - json_type_helper = None - _render_in = 'html', 'xlsx', 'csv', 'json' - _visible = True - - @property - def render_in(self): - """Target(s) in which the field should be rendered as a column. - - Can be set to a callable, which will be called with the column instance. - - Returns: - tuple(str): Renderer identifiers. - """ - resolved = self._render_in - if callable(resolved): - resolved = resolved(self) - return tuple(tolist(resolved)) - - @render_in.setter - def render_in(self, val): - self._render_in = val - - @property - def visible(self): - """Enables column to be rendered to any target in `render_in`. - - Can be set to a callable, which will be called with the column instance. - - Returns: - bool: Enable render. - """ - resolved = self._visible - if callable(resolved): - resolved = resolved(self) - return resolved - - @visible.setter - def visible(self, val): - self._visible = val - - def __new__(cls, *args, **kwargs): - col_inst = super(Column, cls).__new__(cls) - if '_dont_assign' not in kwargs: - col_inst._assign_to_grid() - return col_inst - - def _assign_to_grid(self): - """Columns being set up in declarative fashion need to be attached to the class - somewhere. In WebGrid, we have a class attribute `__cls_cols__` that columns - append themselves to. Subclasses, use of mixins, etc. will combine these column - lists elsewhere. - """ - grid_locals = sys._getframe(2).f_locals - grid_cls_cols = grid_locals.setdefault('__cls_cols__', []) - grid_cls_cols.append(self) - - def __init__(self, label, key=None, filter=None, can_sort=True, # noqa: C901 - xls_width=None, xls_num_format=None, render_in=_None, has_subtotal=False, - visible=True, group=None, **kwargs): - self.label = label - self.key = key - self.filter = filter - self.filter_for = None - self.filter_op = None - self._create_order = False - self.can_sort = can_sort - self.has_subtotal = has_subtotal - self.kwargs = kwargs - self.grid = None - self.expr = None - self._query_idx = None - self._query_key = None - if render_in is not _None: - self.render_in = render_in - self.visible = visible - if xls_width: - self.xls_width = xls_width - if xls_num_format: - self.xls_num_format = xls_num_format - - try: - is_group_cls = issubclass(type(group), ColumnGroup) or issubclass(group, ColumnGroup) - except TypeError: - is_group_cls = False - - if group is not None and not is_group_cls: - raise ValueError(_('expected group to be a subclass of ColumnGroup')) - - self.group = group - - # if the key isn't a base string, assume its a column-like object that - # works with a SA Query instance - if key is None: - self.can_sort = False - elif not isinstance(key, six.string_types): - self.expr = col = key - # use column.key, column.name, or None in that order - key = getattr(col, 'key', getattr(col, 'name', None)) - - if key is None: - raise ValueError(_('expected filter to be a SQLAlchemy column-like' - ' object, but it did not have a "key" or "name"' - ' attribute')) - self.key = self._query_key = key - - # filters can be sent in as a class (not class instance) if needed - if inspect.isclass(filter): - if self.expr is None: - raise ValueError(_('the filter was a class type, but no' - ' column-like object is available from "key" to pass in as' - ' as the first argument')) - self.filter = filter(self.expr) - - def new_instance(self, grid): - """Create a "copy" instance that is linked to a grid instance. - - Used during the grid instantiation process. Grid classes have column instances defining - the grid structure. When the grid instantiates, we have to copy those column instances - along with it, to attach them to the grid instance. - """ - cls = self.__class__ - key = grid.get_unique_column_key( - self.key - or case_cw2us(str(self.label).replace(' ', '')) - or 'unnamed_expression' - ) - - column = cls(self.label, key, None, self.can_sort, group=self.group, _dont_assign=True) - column.key = key - column.grid = grid - column.expr = self.expr - column._query_key = self._query_key - - if self.filter: - column.filter = self.filter.new_instance( - dialect=grid.manager.db.engine.dialect, col=column.expr - ) - - column.head = BlankObject() - column.head.hah = HTMLAttributes(self.kwargs) - column.body = BlankObject() - column.body.hah = HTMLAttributes(self.kwargs) - - # try to be smart about which attributes should get copied to the - # new instance by looking for attributes on the class that have the - # same name as arguments to the classes __init__ method - args = (inspect.getargspec(self.__init__).args - if six.PY2 else inspect.getfullargspec(self.__init__).args) - - for argname in args: - if argname != 'self' and argname not in ( - 'label', 'key', 'filter', 'can_sort', 'render_in', 'visible' - ) and hasattr(self, argname): - setattr(column, argname, getattr(self, argname)) - - # Copy underlying value of render_in and visible, in case they are - # lambdas that should be called per grid instance. - column.render_in = self._render_in - column.visible = self._visible - - return column - - def extract_and_format_data(self, record): - """ - Extract a value from the record for this column and run it through - the data formaters. - """ - data = self.extract_data(record) - data = self.format_data(data) - for _filter, cname in self.grid._colfilters: - for_column = self.grid.column(cname) - if self.key == for_column.key: - data = _filter(self.grid, data) - return data - - def extract_data(self, record): # noqa: C901 - """ - Locate the data for this column in the record and return it. - """ - # key style based on key - try: - if isinstance(record, dict): - return record[self.key] - return record._mapping[self.key] - except (TypeError, KeyError, AttributeError): - pass - - # index style based on position in query and key - if ( - self._query_idx is not None - and hasattr(record, '_fields') - ): - try: - if record._fields[self._query_idx] == self._query_key: - return record[self._query_idx] - except IndexError: - pass - - # attribute style - try: - return getattr(record, self._query_key) - except AttributeError as e: - if ("object has no attribute '%s'" % self._query_key) not in str(e): - raise - except TypeError as e: - if 'attribute name must be string' not in str(e): - raise - - # attribute style with grid key - try: - return getattr(record, self.key) - except AttributeError as e: - if ("object has no attribute '%s'" % self.key) not in str(e): - raise - - raise ExtractionError(_('key "{key}" not found in record', key=self.key)) - - def format_data(self, value): - """ - Use to adjust the value extracted from the record for this column. - By default, no change is made. Useful in sub-classes. - """ - return value - - def render(self, render_type, record, *args, **kwargs): - """Entrypoint from renderer. - - Uses any renderer-specific overrides from the column, or else falls back to - the output of `extract_and_format_data`. - - Renderer-specific methods are expected to be named `render_`, - e.g. `render_html` or `render_xlsx`. - """ - render_attr = 'render_{0}'.format(render_type) - if hasattr(self, render_attr): - return getattr(self, render_attr)(record, *args, **kwargs) - return self.extract_and_format_data(record) - - def apply_sort(self, query, flag_desc): - """Query modifier to enable sort for this column's expression.""" - if self.expr is None: - direction = 'DESC' if flag_desc else 'ASC' - return query.order_by(sasql.text('{0} {1}'.format(self.key, direction))) - if flag_desc: - return query.order_by(self.expr.desc()) - return query.order_by(self.expr) - - def __repr__(self): - return ''.format(self) - - def xls_width_calc(self, value): - """Calculate a width to use for an Excel renderer. - - Defaults to the `xls_width` attribute, if it is set to a non-zero value. Otherwise, - use the length of the stringified value. - """ - if self.xls_width: - return self.xls_width - if isinstance(value, six.string_types): - return len(value) - return len(str(value)) - - -class LinkColumnBase(Column): - """Base class for columns rendering as links in HTML. - - Expects a subclass to supply a `create_url` method for defining the link target. - - Notable args: - link_label (str, optional): Caption to use instead of extracted data from the record. - - Class attributes: - link_attrs (dict): Additional attributes to render on the A tag. - - """ - link_attrs = {} - - def __init__(self, label, key=None, filter=None, can_sort=True, - link_label=None, xls_width=None, xls_num_format=None, - render_in=_None, has_subtotal=False, visible=True, group=None, **kwargs): - super().__init__(label, key, filter, can_sort, xls_width, - xls_num_format, render_in, has_subtotal, visible, - group=group, **kwargs) - self.link_label = link_label - - def link_to(self, label, url, **kwargs): - """Basic render of an anchor tag.""" - return self.grid.html._render_jinja( - '{{label}}', - url=url, - attrs=kwargs, - label=label - ) - - def render_html(self, record, hah): - """Renderer override for HTML to set up a link rather than using the raw data value.""" - url = self.create_url(record) - if self.link_label is not None: - label = self.link_label - else: - label = self.extract_and_format_data(record) - return self.link_to(label, url, **self.link_attrs) - - def create_url(self, record): - """Generate a URL from the given record. - - Expected to be overridden in subclass. - """ - raise NotImplementedError('create_url() must be defined on a subclass') - - -class BoolColumn(Column): - """Column rendering values as True/False (or the given labels). - - Notable args: - reverse (bool, optional): Switch true/false cases. - - true_label (str, optional): String to use for the true case. - - false_label (str, optional): String to use for the false case. - """ - json_type_helper = 'boolean' - - def __init__(self, label, key_or_filter=None, key=None, can_sort=True, - reverse=False, true_label=_('True'), false_label=_('False'), - xls_width=None, xls_num_format=None, render_in=_None, has_subtotal=False, - visible=True, group=None, **kwargs): - super().__init__(label, key_or_filter, key, can_sort, xls_width, - xls_num_format, render_in, - has_subtotal, visible, group=group, **kwargs) - self.reverse = reverse - self.true_label = true_label - self.false_label = false_label - - def format_data(self, data): - if self.reverse: - data = not data - if data: - return self.true_label - return self.false_label - - -class YesNoColumn(BoolColumn): - """BoolColumn rendering values as Yes/No. - - Notable args: - reverse (bool, optional): Switch true/false cases. - - """ - - def __init__(self, label, key_or_filter=None, key=None, can_sort=True, - reverse=False, xls_width=None, xls_num_format=None, render_in=_None, - has_subtotal=False, visible=True, group=None, **kwargs): - super().__init__(label, key_or_filter, key, can_sort, reverse, - _('Yes'), _('No'), xls_width, xls_num_format, - render_in, has_subtotal, visible, group=group, **kwargs) - - -class DateColumnBase(Column): - """Base column for rendering date values in specified formats. - - Designed to work with Python date/datetime/time and Arrow. - - Notable args/attributes: - html_format (str, optional): Date format string for HTML. - - csv_format (str, optional): Date format string for CSV. - - xls_num_format (str, optional): Date format string for Excel. - - """ - - def __init__(self, label, key_or_filter=None, key=None, can_sort=True, - html_format=None, csv_format=None, xls_width=None, - xls_num_format=None, render_in=_None, has_subtotal=False, visible=True, group=None, - **kwargs): - super().__init__(label, key_or_filter, key, can_sort, xls_width, - xls_num_format, render_in, has_subtotal, - visible, group=group, **kwargs) - if html_format: - self.html_format = html_format - - if csv_format: - self.csv_format = csv_format - - def _format_datetime(self, data, format): - # if we have an arrow date, allow html_format to use that functionality - if arrow and isinstance(data, arrow.Arrow): - if data.strftime(format) == format: - return data.format(format) - return data.strftime(format) - - def render_html(self, record, hah): - data = self.extract_and_format_data(record) - if not data: - return data - return self._format_datetime(data, self.html_format) - - def render_xlsx(self, record): - data = self.extract_and_format_data(record) - if not data: - return data - # if we have an arrow date, pull the underlying datetime, else the renderer won't know - # how to handle it - if arrow and isinstance(data, arrow.Arrow): - data = data.datetime - # Excel can't use timezone data, so strip it out - if isinstance(data, dt.datetime) and data.tzinfo is not None: - data = data.replace(tzinfo=None) - return data - - def render_csv(self, record): - data = self.extract_and_format_data(record) - if not data: - return data - return self._format_datetime(data, self.csv_format) - - def xls_width_calc(self, value): - """Determine approximate width from value. - - Value will be a date or datetime object, format as if it was going - to be in HTML as an approximation of its format in Excel. - """ - if self.xls_width: - return self.xls_width - try: - html_version = value.strftime(self.html_format) - return len(html_version) - except AttributeError as e: - if "has no attribute 'strftime'" not in str(e): - raise - # must be the column heading - return Column.xls_width_calc(self, value) - - -class DateColumn(DateColumnBase): - """Column for rendering date values in specified formats. - - Designed to work with Python date and Arrow. - - Notable args/attributes: - html_format (str, optional): Date format string for HTML. - - csv_format (str, optional): Date format string for CSV. - - xls_num_format (str, optional): Date format string for Excel. - - """ - # !!!: localize - html_format = '%m/%d/%Y' - csv_format = '%Y-%m-%d' - xls_num_format = 'm/dd/yyyy' - json_type_helper = 'date' - - -class DateTimeColumn(DateColumnBase): - """Column for rendering datetime values in specified formats. - - Designed to work with Python datetime and Arrow. - - Notable args/attributes: - html_format (str, optional): Date format string for HTML. - - csv_format (str, optional): Date format string for CSV. - - xls_num_format (str, optional): Date format string for Excel. - - """ - # !!!: localize - html_format = '%m/%d/%Y %I:%M %p' - csv_format = '%Y-%m-%d %H:%M:%S%z' - xls_num_format = 'mm/dd/yyyy hh:mm am/pm' - json_type_helper = 'datetime' - - -class TimeColumn(DateColumnBase): - """Column for rendering time values in specified formats. - - Designed to work with Python time and Arrow. - - Notable args/attributes: - html_format (str, optional): Date format string for HTML. - - csv_format (str, optional): Date format string for CSV. - - xls_num_format (str, optional): Date format string for Excel. - - """ - # !!!: localize - html_format = '%I:%M %p' - csv_format = '%H:%M' - xls_num_format = 'hh:mm am/pm' - json_type_helper = 'time' - - -class NumericColumn(Column): - """Column for rendering formatted number values. - - Notable args: - format_as (str, optional): Generic formats. Default "general". - - general: thousands separator and decimal point - - accounting: currency symbol, etc. - - percent: percentage symbol, etc. - - places (int, optional): Decimal places to round to for general. Default 2. - - curr (str, optional): Currency symbol for general. Default empty string. - - sep (str, optional): Thousands separator. Default empty string. - - dp (str, optional): Decimal separator. Default empty string. - - pos (str, optional): Positive number indicator. Default empty string. - - neg (str, optional): Negative number indicator for general. Default empty string. - - trailneg (str, optional): Negative number suffix. Default empty string. - - xls_neg_red (bool, optional): Renders negatives in red for Excel. Default True. - - Class attributes: - `xls_fmt_general`, `xls_fmt_accounting`, `xls_fmt_percent` are Excel number - formats used for the corresponding `format_as` setting. - """ - # !!!: localize - xls_fmt_general = '#,##0{dec_places};{neg_prefix}-#,##0{dec_places}' - xls_fmt_accounting = '_($* #,##0{dec_places}_);{neg_prefix}_($* (#,##0{dec_places})' + \ - ';_($* "-"??_);_(@_)' - xls_fmt_percent = '0{dec_places}%;{neg_prefix}-0{dec_places}%' - - def __init__(self, label, key_or_filter=None, key=None, can_sort=True, - reverse=False, xls_width=None, xls_num_format=None, - render_in=_None, format_as='general', places=2, curr='', - sep=',', dp='.', pos='', neg='-', trailneg='', - xls_neg_red=True, has_subtotal=False, visible=True, group=None, **kwargs): - super().__init__(label, key_or_filter, key, can_sort, xls_width, - xls_num_format, render_in, - has_subtotal, visible, group=group, **kwargs) - self.places = places - self.curr = curr - self.sep = sep - self.dp = dp - self.pos = pos - self.neg = neg - self.trailneg = trailneg - self.xls_neg_red = xls_neg_red - self.format_as = format_as - self.json_type_helper = f'number_{format_as}' - - def html_decimal_format_opts(self, data): - """Return tuple of options to expand for decimalfmt arguments. - - `places`, `curr`, `neg`, and `trailneg` attributes are passed through unless `format_as` - is "accounting". - """ - return ( - 2 if self.format_as == 'accounting' else self.places, - '$' if self.format_as == 'accounting' else self.curr, - self.sep, - self.dp, - self.pos, - '(' if self.format_as == 'accounting' else self.neg, - ')' if self.format_as == 'accounting' else self.trailneg, - ) - - def render_html(self, record, hah): - """HTML render override for numbers. - - If format is percent, the value is multiplied by 100 to get the render value. - - Negative values are given a "negative" CSS class in the render. - """ - data = self.extract_and_format_data(record) - if not data and data != 0: - return data - - if self.format_as == 'percent': - data = data * 100 - - formatted = decimalfmt(data, *self.html_decimal_format_opts(data)) - - if self.format_as == 'percent': - formatted += '%' - - if data < 0: - hah.class_ += 'negative' - - return formatted - - def xls_construct_format(self, fmt_str): - """Apply places and xls_neg_red settings to the given number format string.""" - neg_prefix = '[RED]' if self.xls_neg_red else '' - dec_places = '.'.ljust(self.places + 1, '0') if self.places else '' - return fmt_str.format(dec_places=dec_places, neg_prefix=neg_prefix) - - def get_num_format(self): - """Match format_as setting to one of the format strings in class attributes.""" - if self.format_as == 'general': - return self.xls_construct_format(self.xls_fmt_general) - if self.format_as == 'percent': - return self.xls_construct_format(self.xls_fmt_percent) - if self.format_as == 'accounting': - return self.xls_construct_format(self.xls_fmt_accounting) - return None - - @property - def xlsx_style(self): - """Number format for XLSX target.""" - return { - 'num_format': self.get_num_format() - } - - -class EnumColumn(Column): - """ - This column type is meant to be used with python `enum.Enum` type columns. It expects that - the display value is the `value` attribute of the enum instance. - """ - def format_data(self, value): - if value is None: - return None - return value.value - - -class ColumnGroup(object): - r"""Represents a grouping of grid columns which may be rendered within a group label. - - Args: - label (str): Grouping label to be rendered for the column set. - class\_ (str): CSS class name to apply in HTML rendering. - """ # noqa: W605 - label = None - class_ = None - - def __init__(self, label, class_=None): - self.label = label - self.class_ = class_ - - -class QueryStringBuilder: - arg_factories = ( - 'session', - 'search', - 'paging', - 'sort', - 'filter', - ) - - def __init__(self, grid, include_session=False): - self.grid = grid - self.include_session = include_session - - def __call__(self): - return self.build() - - def args_session(self): - if not self.include_session: - return [] - return [('session_key', self.grid.session_key)] - - def args_search(self): - if not self.grid.search_value: - return [] - return [('search', self.grid.search_value)] - - def args_paging(self): - grid_args = [] - if self.grid.on_page != self.grid.__class__.on_page: - grid_args.append(('onpage', self.grid.on_page)) - if self.grid.per_page != self.grid.__class__.per_page: - grid_args.append(('perpage', self.grid.per_page)) - return grid_args - - def args_sort(self): - # sort args are stored as tuple of (key, flag_desc) - return map( - lambda item: (f'sort{item[0]}', ('-' if item[1][1] else '') + item[1][0]), - enumerate(self.grid.order_by, 1), - ) - - def args_filter(self): - # for any filters, we only want to include args if the filter is set - grid_args = [] - for col in self.grid.filtered_cols.values(): - _filter = col.filter - if not _filter.is_active: - continue - grid_args.append((f'op({col.key})', _filter.op)) - if _filter.value1: - grid_args.extend(map( - lambda item: (f'v1({col.key})', item), - tolist(_filter.value1), - )) - if _filter.value2: - grid_args.append((f'v2({col.key})', _filter.value2)) - return grid_args - - def build(self): - grid_args = [] - for _factory_key in self.arg_factories: - grid_args.extend(getattr(self, f'args_{_factory_key}')()) - - # note: sorting is not strictly necessary but ensures order of keys for testing - return urllib.parse.urlencode(sorted(grid_args), safe='()') - - -class BaseGrid(six.with_metaclass(_DeclarativeMeta, object)): - r"""WebGrid grid base class. - - Handles class declarative-style grid description of columns, filterers, and rendering. - - The constructor is responsible for: - - - setting initial attributes - - initializing renderers - - setting up columns for the grid instance - - running the grid's `post_init` method - - Args: - ident (str, optional): Identifier value for ident instance property. Defaults to None. - - per_page (int, optional): Default number of records per page. Defaults to _None. - - on_page (int, optional): Default starting page. Defaults to _None. - - qs_prefix (str, optional): Arg name prefix to apply in query string. Useful for having - multiple unconnected grids on a single page. Defaults to ''. - - class\_ (str, optional): CSS class name for main grid div. Defaults to 'datagrid'. - - Class Attributes: - identifier (str): Optional string identifier used for the ident property. - - sorter_on (bool): Enable HTML sorting UX. Default True. - - pager_on (bool): Enable record limits in queries and HTML pager UX. Default True. - - per_page (int): Default number of records per page, can be overridden in constructor - or through query string args. Default 50. - - on_page (int): Default page number, can be overridden in constructor or through - query string args. Default 1. - - hide_controls_box (bool): Hides HTML filter/page/sort/count UX. Default False. - - session_on (bool): Enable web context session storage of grid filter/page/sort args. - Default True. - - subtotals (string): Enable subtotals. Can be none|page|grand|all. Default "none". - - manager (Manager): Framework plugin for the web context, such as webgrid.flask.WebGrid. - - allowed_export_targets (dict[str, Renderer]): Map non-HTML export targets to the - Renderer classes. - - enable_search (bool): Enable single-search UX. Default True. - - unconfirmed_export_limit (int): Ask for confirmation before exporting more than this many - records. Set to None to disable. Default 10000. - - query_select_from (selectable): Entity, table, or other selectable(s) to use as the query - from. If attributes like query_filter are used along with select_from, SQLAlchemy may - require the select_from to precede the filtering. - - query_joins (tuple): Tuple of joins to bring the query together for all columns. May - have just the join object, or also conditions. - e.g. [Blog], ([Blog.category], ), or [(Blog, Blog.active == sa.true())] - Note, relationship attributes must be referenced within tuples, due to SQLAlchemy magic. - - query_outer_joins (tuple): Tuple of outer joins. See query_joins. - - query_filter (tuple): Filter parameter(s) tuple to be used on the query. - Note, relationship attributes must be referenced within tuples, due to SQLAlchemy magic. - - query_default_sort (tuple): Parameter(s) tuple to be passed to order_by if sort options - are not set on the grid. - Note, relationship attributes must be referenced within tuples, due to SQLAlchemy magic. - - """ # noqa: W605 - __cls_cols__ = () - identifier = None - sorter_on = True - pager_on = True - per_page = 50 - on_page = 1 - hide_controls_box = False - # enables keyed session store of grid arguments - session_on = True - # enables page/grand subtotals: none|page|grand|all - subtotals = 'none' - manager = None - allowed_export_targets = None - # Enables single-search feature, where one search value is applied to every supporting - # filter at once - enable_search = True - - # Base selectable(s) to be used in the FROM clause of the query - query_select_from = None - # List of joins to bring the query together for all columns. May have just the join object, - # or also conditions - # e.g. [Blog], ([Blog.category], ), or [(Blog, Blog.active == sa.true())] - # note: relationship attributes must be referenced within tuples, due to SQLAlchemy magic - query_joins = None - query_outer_joins = None - # Filter parameter(s) tuple to be used on the query - # note: relationship attributes must be referenced within tuples, due to SQLAlchemy magic - query_filter = None - # Parameter(s) tuple to be passed to order_by if sort options are not set on the grid - # note: relationship attributes must be referenced within tuples, due to SQLAlchemy magic - query_default_sort = None - - # Will ask for confirmation before exporting more than this many records. - # Set to None to disable this check - unconfirmed_export_limit = 10000 - - def __init__(self, ident=None, per_page=_None, on_page=_None, qs_prefix='', class_='datagrid', - **kwargs): - self._ident = ident - self.hah = HTMLAttributes(kwargs) - self.hah.id = self.ident - self.hah.class_ += class_ - self.filtered_cols = OrderedDict() - self.subtotal_cols = OrderedDict() - self.order_by = [] - self.qs_prefix = qs_prefix - self.user_warnings = [] - self.search_value = None - self._record_count = None - self._records = None - self._page_totals = None - self._grand_totals = None - - if self.allowed_export_targets is None: - self.allowed_export_targets = {} - # If the grid doesn't define any export targets - # lets setup the export target for xlsx if we have the requirement - if openpyxl or xlsxwriter: - self.allowed_export_targets['xlsx'] = XLSX - self.set_renderers() - self.export_to = None - # when session feature is enabled, key is the unique string - # used to distinguish grids. Initially set to a random - # string, but will be set to the session key in args - self.session_key = randchars(12) - # at times, different grids may be made to share a session - self.foreign_session_loaded = False - - self.per_page = per_page if per_page is not _None else self.__class__.per_page - self.on_page = on_page if on_page is not _None else self.__class__.on_page - - self.columns = [] - self.key_column_map = {} - - self._init_columns() - self.post_init() - - def _init_columns(self): - """Create column instances to attach to a grid instance. - - Columns set up in the declarative grid description are instances bound to the grid - class. When the grid is instantiated, those column instances need to be copied over - to the grid instance. - - Columns are responsible for their own "copy" process with the `new_instance` method. - """ - for col in self.__cls_cols__: - self.add_column(col) - - def add_column(self, column): - new_col = column.new_instance(self) - self.columns.append(new_col) - self.key_column_map[new_col.key] = new_col - if new_col.filter is not None: - self.filtered_cols[new_col.key] = new_col - if new_col.has_subtotal is not False and new_col.has_subtotal is not None: - self.subtotal_cols[new_col.key] = ( - subtotal_function_map(new_col.has_subtotal), - new_col - ) - - def drop_columns(self, column_keys): - self.columns = list(filter( - lambda col: col.key not in tolist(column_keys), - self.columns - )) - for key in tolist(column_keys): - self.key_column_map.pop(key, None) - self.filtered_cols.pop(key, None) - self.subtotal_cols.pop(key, None) - - def post_init(self): - """Provided for subclasses to run post-initialization customizations. - """ - pass - - def set_column_order(self, column_keys): - """Most renderers output columns in the order they appear in the grid's ``columns`` - list. When bringing mixins together or subclassing a grid, however, the order is - often not what is intended. - - This method allows a manual override of column order, based on keys.""" - key_check = set(column_keys) - set(self.key_column_map.keys()) - if key_check: - raise Exception(f'Keys not recognized on grid: {key_check}') - - self.columns = [ - self.key_column_map[key] for key in column_keys - ] - - def before_query_hook(self): - """Hook to give subclasses a chance to change things before executing the query. - """ - pass - - def build(self, grid_args=None): - """Apply query args, run `before_query_hook`, and execute a record count query. - - Calling `build` is preferred to simply calling `apply_qs_args` in a view. Otherwise, - AttributeErrors can be hidden when the grid is used in Jinja templates. - """ - self.apply_qs_args(grid_args=grid_args) - self.before_query_hook() - # this will force the query to execute. We used to wait to evaluate this but it ended - # up causing AttributeErrors to be hidden when the grid was used in Jinja. - # Calling build is now preferred over calling .apply_qs_args() and then .html() - self.record_count - - def check_auth(self): - """For API usage, provides a hook for grids to specify authorization that should be - applied for the API responder method. - - If a 40* response is warranted, take that action here. - - Note, this method is not part of normal grid/render operation. It will only be - executed if run by a calling layer, such as the Flask WebGridAPI manager/extension. - """ - pass - - def column(self, ident): - """Retrieve a grid column instance via either the key string or index int. - - Args: - ident (Union[str, int]): Key/index for lookup. - - Returns: - Column: Instance column matching the ident. - - Raises: - KeyError when ident is a string not matching any column. - - IndexError when ident is an int but out of bounds for the grid. - """ - if isinstance(ident, six.string_types): - return self.key_column_map[ident] - return self.columns[ident] - - def has_column(self, ident): - """Verify string key or int index is defined for the grid instance. - - Args: - ident (Union[str, int]): Key/index for lookup. - - Returns: - bool: Indicates whether key/index is in the grid columns. - - """ - if ident is None: - return False - if isinstance(ident, six.string_types): - return ident in self.key_column_map - return 0 <= ident < len(self.columns) - - def get_unique_column_key(self, key): - """Apply numeric suffix to a field key to make the key unique to the grid. - - Helpful for when multiple entities are represented in grid columns but have - the same field names. - - For instance, Blog.label and Author.label both have the field name `label`. - The first column will have the `label` key, and the second will get `label_1`. - - Args: - key (str): field key to make unique. - - Returns: - str: unique key that may be assigned in the grid's `key_column_map`. - """ - suffix_counter = 0 - new_key = key - while self.has_column(new_key): - suffix_counter += 1 - new_key = '{}_{}'.format(key, suffix_counter) - return new_key - - def iter_columns(self, render_type): - """Generator yielding columns that are visible and enabled for target `render_type`. - - Args: - render_type (str): [description] - - Yields: - Column: Grid instance's column instance that is renderable for `render_type`. - """ - for col in self.columns: - if col.visible and render_type in col.render_in: - yield col - - def can_search(self): - """Grid `enable_search` attr turns on search, but check if there are supporting filters. - - Returns: - bool: search enabled and supporting filters exist - """ - return self.enable_search and len(self.search_expression_generators) > 0 - - @property - def search_expression_generators(self): - """Get single-search query modifier factories from the grid filters. - - Raises: - Exception: filter's `get_search_expr` did not return None or callable - - Returns: - tuple(callable): search expression callables from grid filters - """ - is_aggregate = self.search_uses_aggregate - - # We filter out None here so as to disregard filters that don't support the search feature. - def check_expression_generator(expr_gen): - if expr_gen is not None and not callable(expr_gen): - raise Exception( - 'bad filter search expression: {} is not callable'.format(str(expr_gen)) - ) - return expr_gen is not None - - # Also conditionally filter aggregate/non-aggregate so we're not mixing expression types. - return tuple(filter( - check_expression_generator, - [col.filter.get_search_expr() for col in self.filtered_cols.values() - if col.filter.is_aggregate is is_aggregate] - )) - - @property - def search_uses_aggregate(self): - """Determine whether search should use aggregate filtering. - - By default, only use the HAVING clause if all search-enabled filters are marked - as aggregate. Otherwise, we'd be requiring all grid columns to be in query - grouping. If there are filters for search that are not aggregate, the grid will - only search on the non-aggregate columns. - - Returns: - bool: search aggregate usage determined from filter info - """ - has_search = False - for col in self.filtered_cols.values(): - if col.filter.get_search_expr() is not None: - has_search = True - if not col.filter.is_aggregate: - return False - return has_search - - def set_renderers(self): - """Renderers assigned as attributes on the grid instance, named by render target. - """ - self.html = HTML(self) - for key, value in self.allowed_export_targets.items(): - setattr(self, key, value(self)) - - def set_filter(self, key, op, value, value2=None): - """Set filter parameters on a column's filter. Resets record cache. - - Args: - key (str): Column identifier - op (str): Operator - value (Any): First filter value - value2 (Any, optional): Second filter value if applicable. Defaults to None. - """ - self.clear_record_cache() - self.filtered_cols[key].filter.set(op, value, value2=value2) - - def set_sort(self, *args): - """Set sort parameters for main query. Resets record cache. - - If keys are passed in that do not belong to this grid, raise user warnings - (not exceptions). These warnings are suppressed if the grid has a "foreign" - session assigned (i.e. two grids share some of the same columns, and should - load as much information as possible from the shared session key). - - Args: - Each arg is expected to be a column key. If the sort is to be descending for - that key, prepend with a "-". - E.g. `grid.set_sort('author', '-post_date')` - """ - self.clear_record_cache(preserve_count=True) - self.order_by = [] - - for key in args: - if not key: - continue - flag_desc = False - if key.startswith('-'): - flag_desc = True - key = key[1:] - if key in self.key_column_map and self.key_column_map[key].can_sort: - self.order_by.append((key, flag_desc)) - elif not self.foreign_session_loaded: - self.user_warnings.append(_('''can't sort on invalid key "{key}"''', key=key)) - - def set_paging(self, per_page, on_page): - """Set paging parameters for the main query. Resets record cache. - - Args: - per_page (int): Record limit for each page. - on_page (int): With `per_page`, computes the offset. - """ - self.clear_record_cache(preserve_count=True) - self.per_page = per_page - self.on_page = on_page - - def clear_record_cache(self, preserve_count=False): - """Reset records and record count cached from previous queries. - - Args: - preserve_count (bool): Direct grid to retain count of records, effectively removing - only the table of records itself. - """ - if not preserve_count: - self._record_count = None - self._records = None - - @property - def ident(self): - return self._ident \ - or self.identifier \ - or case_cw2us(self.__class__.__name__) - - @property - def default_session_key(self): - return f'_{self.__class__.__name__}' - - @property - def has_filters(self): - """Indicates whether filters will be applied in `build_query`. - - Returns: - bool: True if filter(s) have op/value set or single search value is given. - """ - for col in six.itervalues(self.filtered_cols): - if col.filter.is_active: - return True - return self.search_value is not None - - @property - def has_sort(self): - """Indicates whether ordering will be applied in `build_query`. - - Returns: - bool: True if grid's `order_by` list is populated. - """ - return bool(self.order_by) - - @property - def record_count(self): - """Count of records for current filtered query. - - Value is cached to prevent duplicate query execution. Methods changing - the query (e.g. `set_filter`) will reset the cached value. - - Returns: - int: Count of records. - """ - if self._record_count is None: - query = self.build_query(for_count=True) - t0 = time.perf_counter() - self._record_count = query.count() - t1 = time.perf_counter() - log.debug('Count query ran in {} seconds'.format(t1 - t0)) - return self._record_count - - @property - def records(self): - """Records returned for current filtered/sorted/paged query. - - Result is cached to prevent duplicate query execution. Methods changing - the query (e.g. `set_filter`) will reset the cached result. - - Returns: - list(Any): Result records from SQLAlchemy query. - """ - if self._records is None: - query = self.build_query() - t0 = time.perf_counter() - self._records = query.all() - t1 = time.perf_counter() - log.debug('Data query ran in {} seconds'.format(t1 - t0)) - return self._records - - def _totals_col_results(self, page_totals_only): - """Executes query to retrieve subtotals for the filtered query. - - A single result record is returned, which will have fields corresponding to all of the - grid columns (same as a record returned in the general records query). - - Args: - page_totals_only (bool): Tells query builder to use only current page records. - - Returns: - Any: Single result record. - """ - SUB = self.build_query(for_count=(not page_totals_only)).subquery() - - cols = [] - # Not all columns can be totaled. But, we should put in null placeholders - # for any untotaled columns, so that the same query indices from query_base - # can be applied. - # This will apply to any columns with an expr. Other subtotaled columns can be - # tacked onto the end - these will not be indexed and must be referred to by name - for colobj in [ - col for col in self.columns if col.expr is not None - ] + [ - coltuple[1] for _, coltuple in self.subtotal_cols.items() if coltuple[1].expr is None - ]: - colname = colobj._query_key or colobj.key - - if colobj.key not in self.subtotal_cols: - cols.append( - sa.literal(None).label(colname) - ) - continue - - sa_aggregate_func, _ = self.subtotal_cols[colobj.key] - - # column may have a label. If it does, use it - if isinstance(colobj.expr, sasql.expression.Label): - aggregate_this = sasql.text(colobj.key) - elif colobj.expr is None: - aggregate_this = sasql.literal_column(colobj.key) - else: - aggregate_this = colobj.expr - - # sa_aggregate_func could be an expression, or a callable. If it is callable, give it - # the column - labeled_aggregate_col = None - if callable(sa_aggregate_func): - labeled_aggregate_col = sa_aggregate_func(aggregate_this).label(colname) - elif isinstance(sa_aggregate_func, six.string_types): - labeled_aggregate_col = sasql.literal_column(sa_aggregate_func).label(colname) - else: - labeled_aggregate_col = sa_aggregate_func.label(colname) - cols.append(labeled_aggregate_col) - cols.append(sa.literal(1).label('__is_total__')) - - t0 = time.perf_counter() - query = self.manager.sa_query(*cols) - # WARN: probably not future proof - query._set_select_from([SUB], True) - result = query.first() - t1 = time.perf_counter() - log.debug('Totals query ran in {} seconds'.format(t1 - t0)) - - return result - - @property - def page_totals(self): - """Executes query to retrieve subtotals for the filtered query on the current page. - - For page totals to be queried/returned, the grid's `subtotals` must be page/all - and one or more columns must have subtotals configured. - - A single result record is returned, which will have fields corresponding to all of the - grid columns (same as a record returned in the general records query). - - Returns: - Any: Single result record, or None if page totals are not configured. - """ - if ( - self._page_totals is None - and self.subtotals in ('page', 'all') - and self.subtotal_cols - ): - self._page_totals = self._totals_col_results(page_totals_only=True) - return self._page_totals - - @property - def grand_totals(self): - """Executes query to retrieve subtotals for the filtered query. - - For grand totals to be queried/returned, the grid's `subtotals` must be grand/all - and one or more columns must have subtotals configured. - - A single result record is returned, which will have fields corresponding to all of the - grid columns (same as a record returned in the general records query). - - Returns: - Any: Single result record, or None if grand totals are not configured. - """ - if ( - self._grand_totals is None - and self.subtotals in ('grand', 'all') - and self.subtotal_cols - ): - self._grand_totals = self._totals_col_results(page_totals_only=False) - return self._grand_totals - - @property - def page_count(self): - """Page count, or 1 if no `per_page` is set. - """ - if self.per_page is None: - return 1 - return max(0, self.record_count - 1) // self.per_page + 1 - - def build_query(self, for_count=False): - """Constructs, but does not execute, a grid query from columns and configuration. - - This is the query the grid functions trust for results for records, count, page - count, etc. Customization of the query should happen here or in the methods called - within. - - Build sequence: - - `query_base` - - `query_prep` - - `query_filters` - - `query_sort` - - `query_paging` - - Args: - for_count (bool, optional): Excludes sort/page from query. Defaults to False. - - Returns: - Query: SQLAlchemy query object - """ - log.debug(str(self)) - - has_filters = self.has_filters - query = self.query_base(self.has_sort, has_filters) - query = self.query_prep(query, self.has_sort or for_count, has_filters) - - if has_filters: - query = self.query_filters(query) - else: - log.debug('No filters') - - if for_count: - return query - - query = self.query_sort(query) - if self.pager_on: - query = self.query_paging(query) - - return query - - def set_records(self, records): - """Assign a set of records to the grid's cache. - - Useful for simple grids that simply need to be rendered as a table. Note that any - ops performed on the grid, such as setting filter/sort/page options, will clear this - cached information. - - Args: - records (list(Any)): List of record objects that can be referenced for column data. - """ - self._record_count = len(records) - self._records = records - - def query_base(self, has_sort, has_filters): - """Construct a query from grid columns, using grid's join/filter/sort attributes. - - Used by `build_query` to establish the basic query from column spec. If query is to be - modified, it is recommended to do so in `query_prep` if possible, rather than overriding - `query_base`. - - Args: - has_sort (bool): Tells method not to order query, since the grid has sort params. - has_filters (bool): Tells method if grid has filter params. Not used. - - Returns: - Query: SQLAlchemy query - """ - for idx, column in enumerate(filter(lambda col: col.expr is not None, self.columns)): - column._query_idx = idx - cols = [col.expr for col in self.columns if col.expr is not None] - query = self.manager.sa_query(*cols) - - if self.query_select_from is not None: - query = query.select_from(*tolist(self.query_select_from)) - - for join_terms in (tolist(self.query_joins) or tuple()): - query = query.join(*tolist(join_terms)) - - for join_terms in (tolist(self.query_outer_joins) or tuple()): - query = query.outerjoin(*tolist(join_terms)) - - if self.query_filter: - query = query.filter(*tolist(self.query_filter)) - - if not has_sort and self.query_default_sort is not None: - query = query.order_by(*tolist(self.query_default_sort)) - - return query - - def query_prep(self, query, has_sort, has_filters): - """Modify the query that was constructed in `query_base`. - - Joins, query filtering, and default sorting can be applied via grid attributes. However, - sometimes grid queries need columns added, instance-time modifications applied, etc. - - Called by `build_query`. - - Args: - query (Query): SQLAlchemy query object. - has_sort (bool): Tells method grid has sort params defined. - has_filters (bool): Tells method if grid has filter params. - - Returns: - Query: SQLAlchemy query - """ - return query - - def query_filters(self, query): - """Modify the query by applying filter terms. - - Called by `build_query` to apply any column filters as needed. Also enacts - the single-search value if specified. - - Args: - query (Query): SQLAlchemy query object. - - Returns: - Query: SQLAlchemy query - """ - filter_display = [] - if self.search_value: - query = self.apply_search(query, self.search_value) - - for col in six.itervalues(self.filtered_cols): - if col.filter.is_active: - filter_display.append('{}: {}'.format(col.key, str(col.filter))) - query = col.filter.apply(query) - if filter_display: - log.debug(';'.join(filter_display)) - else: - log.debug('No filters') - return query - - def apply_search(self, query, value): - """Modify the query by applying a filter term constructed from search clauses. - - Calls each filter search expression factory with the search value to get a search - clause, then ORs them all together for the main query. - - Args: - query (Query): SQLAlchemy query. - value (str): Search value. - - Returns: - Query: SQLAlchemy query - """ - filter_method = query.having if self.search_uses_aggregate else query.filter - filter_clauses = list(filter( - lambda item: item is not None, - (expr(value) for expr in self.search_expression_generators) - )) - - if not filter_clauses: - return query - return filter_method(sa.or_(*filter_clauses)) - - def query_paging(self, query): - """Modify the query by applying limit/offset to match grid parameters. - - Args: - query (Query): SQLAlchemy query. - - Returns: - Query: SQLAlchemy query - """ - if self.on_page and self.per_page: - offset = (self.on_page - 1) * self.per_page - query = query.offset(offset).limit(self.per_page) - log.debug('Page {}; {} per page'.format(self.on_page, self.per_page)) - return query - - def query_sort(self, query): - """Modify the query by applying sort to match grid parameters. - - Args: - query (Query): SQLAlchemy query. - - Returns: - Query: SQLAlchemy query - """ - redundant = [] - sort_display = [] - for key, flag_desc in self.order_by: - if key in self.key_column_map: - col = self.key_column_map[key] - # remove any redundant names, whichever comes first is what we will keep - if col.key in redundant: - continue - else: - sort_display.append(col.key) - redundant.append(col.key) - query = col.apply_sort(query, flag_desc) - if sort_display: - log.debug(','.join(sort_display)) - else: - log.debug('No sorts') - - # Special consideration for MSSQL, because if paging is to work, the query - # must have an ORDER BY clause. This is problematic, because if an app - # does not test for the query case where paging is enabled for page > 1, - # the query will not hit the error state. Fix the case if possible. - if ( - self.pager_on - and self.manager - and self.manager.db.engine.dialect.name == 'mssql' - and not query._order_by_clauses - ): - query = self._fix_mssql_order_by(query) - - return query - - def _fix_mssql_order_by(self, query): - """MSSQL must have an ORDER BY for paging to work. If no sort clause has been - defined, sort by the first column. If that doesn't work, error out. - """ - if len(self.columns): - query = self.columns[0].apply_sort(query, False) - if query._order_by_clauses: - return query - raise Exception( - 'Paging is enabled, but query does not have ORDER BY clause required for MSSQL' - ) - - def build_qs_args(self, include_session=False): - """Build a URL query string based on current grid attributes. - - This is designed to be framework-agnostic and not require a request context. Usually - the result would be used in a background task or similar (i.e. outside the flow of the - rendered grid), so typically the session key is unnecessary. - - Args: - include_session (bool, optional): Include session_key in the string. Defaults to False. - """ - return QueryStringBuilder(self, include_session)() - - def apply_qs_args(self, add_user_warnings=True, grid_args=None): - """Process args from manager for filter/page/sort/export. - - Args: - add_user_warnings (bool, optional): Add flash messages for warnings. Defaults to True. - grid_args (MultiDict, optional): Supply args directly to the grid. - """ - args = grid_args if grid_args is not None else self.manager.get_args(self) - - if self.session_on: - self.session_key = args.get('session_key') or self.session_key - self.foreign_session_loaded = args.get('__foreign_session_loaded__', False) - - # search - self._apply_search(args) - - # filtering (make sure this is above paging otherwise self.page_count - # used in the paging section below won't work) - self._apply_filtering(args) - - # paging - self._apply_paging(args) - - # sorting - self._apply_sorting(args) - - # export - self._apply_export(args) - - # Having this here is not ideal. Due to separation of concerns, it would be nice to - # have flash warnings in the HTML renderer. However, by the time the renderer is - # called, an app template has probably already loaded and rendered any messages to - # be shown, and it's too late to add new ones. - if add_user_warnings: - for msg in self.user_warnings: - self.manager.flash_message('warning', msg) - - def _apply_search(self, args): - if ( - 'search' in args - and self.can_search() - ): - self.search_value = args['search'].strip() if args['search'] else None - - def _apply_filtering(self, args): - """Turn request/session args into filter settings. - - Args: - args (MultiDict): Full arguments to search for filters. - """ - for col in six.itervalues(self.filtered_cols): - filter = col.filter - filter_op_qsk = 'op({0})'.format(col.key) - filter_v1_qsk = 'v1({0})'.format(col.key) - filter_v2_qsk = 'v2({0})'.format(col.key) - - filter_op_value = args.get(filter_op_qsk, None) - - if filter._default_op: - filter.set(None, None, None) - - if filter_op_value is not None: - if filter.receives_list: - v1 = args.getlist(filter_v1_qsk) - v2 = args.getlist(filter_v2_qsk) - else: - v1 = args.get(filter_v1_qsk, None) - v2 = args.get(filter_v2_qsk, None) - - try: - filter.set( - filter_op_value, - v1, - v2, - ) - except validators.ValueInvalid as e: - invalid_msg = filter.format_invalid(e, col) - self.user_warnings.append(invalid_msg) - - def _apply_paging(self, args): - """Turn request/session args into page settings. - - Args: - args (MultiDict): Full arguments to search for paging. - """ - pp_qsk = 'perpage' - if pp_qsk in args: - per_page = self.apply_validator(validators.IntValidator, args[pp_qsk], pp_qsk) - if per_page is None: - per_page = self.__class__.per_page - elif per_page < 1: - per_page = 1 - self.per_page = per_page - - op_qsk = 'onpage' - if op_qsk in args: - on_page = self.apply_validator(validators.IntValidator, args[op_qsk], op_qsk) - if on_page is None or on_page < 1: - on_page = 1 - if on_page > self.page_count: - on_page = self.page_count - self.on_page = on_page - - def _apply_sorting(self, args): - """Turn request/session args into sort settings. - - No limit is present here for how many sort args may be passed. However, the - args are expected to be contiguous. I.e. sort1, sort2, sort3 will be processed, - but sort1, sort3, sort5 will only process sort1. - - Args: - args (MultiDict): Full arguments to search for sort keys. - """ - counter = 0 - sort_qs_values = [] - - while True: - counter += 1 - sort_arg = 'sort{}'.format(counter) - if sort_arg not in args: - break - sort_qs_values.append(args[sort_arg]) - - if sort_qs_values: - self.set_sort(*sort_qs_values) - - def _apply_export(self, args): - # handle other file formats - self.set_export_to(args.get('export_to', None)) - - def prefix_qs_arg_key(self, key): - """Given a bare arg key, return the prefixed version that will actually be in the request. - - This is necessary for render targets that will construct ensuing requests. Prefixing is - not needed for incoming args on internal grid ops, as long as the grid manager's - args loaders sanitize the args properly. - - Args: - key (str): Bare arg key. - - Returns: - str: Prefixed arg key. - """ - return '{0}{1}'.format(self.qs_prefix, key) - - def apply_validator(self, validator, value, qs_arg_key): - """Apply a webgrid validator to value, and produce a warning if invalid. - - Args: - validator (Validator): webgrid validator. - value (str): Value to validate. - qs_arg_key (str): Arg name to include in warning if value is invalid. - - Returns: - Any: Output of `validator.to_python(value)`, or `None` if invalid. - """ - try: - return validator().process(value) - except validators.ValueInvalid: - invalid_msg = _('"{arg}" grid argument invalid, ignoring', arg=qs_arg_key) - self.user_warnings.append(invalid_msg) - return None - - def set_export_to(self, to): - """Set export parameter after validating it exists in known targets. - - Args: - to (str): Renderer attribute if it is known. Invalid value ignored. - """ - if to in self.allowed_export_targets: - self.export_to = to - - def export_as_response(self, wb=None, sheet_name=None): - """Return renderer response for view layer to provide as a file. - - Args: - wb (Workbook, optional): XlsxWriter Workbook. Defaults to None. - sheet_name (Worksheet, optional): XlsxWriter Worksheet. Defaults to None. - - Raises: - ValueError: No export parameter given. - - Returns: - Response: Return response processed through renderer and manager. - """ - if not self.export_to: - raise ValueError('No export format set') - exporter = getattr(self, self.export_to) - if self.export_to == 'xlsx': - return exporter.as_response(wb, sheet_name) - return exporter.as_response() - - def __repr__(self): - return ''.format(self.__class__.__name__) - - -def row_styler(f): - f.__grid_rowstyler__ = True - return f - - -def col_styler(for_column): - def decorator(f): - f.__grid_colstyler__ = for_column - return f - return decorator - - -def col_filter(for_column): - def decorator(f): - f.__grid_colfilter__ = for_column - return f - return decorator diff --git a/webgrid/version.py b/webgrid/version.py deleted file mode 100644 index 1ff6ff2..0000000 --- a/webgrid/version.py +++ /dev/null @@ -1 +0,0 @@ -VERSION = '0.5.10' From 0721b9d634058a58e7cebe1306943bac7734ce28 Mon Sep 17 00:00:00 2001 From: Randy Syring Date: Sat, 2 Aug 2025 15:52:45 -0400 Subject: [PATCH 07/33] Remove legacy files --- MANIFEST.in | 4 ---- pytest.ini | 1 - 2 files changed, 5 deletions(-) delete mode 100644 MANIFEST.in delete mode 100644 pytest.ini diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 41a189d..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,4 +0,0 @@ -recursive-include webgrid * -recursive-include webgrid_ta * -global-exclude *.pyc -include *.rst diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index eea2c18..0000000 --- a/pytest.ini +++ /dev/null @@ -1 +0,0 @@ -[pytest] From 2e2df7b92549c63e097ddd365bda41f0f5f978f9 Mon Sep 17 00:00:00 2001 From: Randy Syring Date: Sat, 2 Aug 2025 15:52:36 -0400 Subject: [PATCH 08/33] pyproject.toml updates --- pyproject.toml | 53 ++++++++++++++++++++++++++++++++++++++--- stable-requirements.txt | 45 ---------------------------------- 2 files changed, 50 insertions(+), 48 deletions(-) delete mode 100644 stable-requirements.txt diff --git a/pyproject.toml b/pyproject.toml index 76ce608..c6d820e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,11 +12,11 @@ description = "A library for rendering HTML tables and Excel files from SQLAlche authors = [ {name = 'Level 12', email = 'devteam@level12.io'}, ] -requires-python = '~=3.10.0' +requires-python = '>=3.10' dynamic = ['version'] readme = { file = "readme.rst", content-type = "text/x-rst" } -license = { text = "BSD-3-Clause" } -urls = { "homepage" = "https://github.com/level12/webgrid" } +license.file = "license.txt" +urls.homepage = "https://github.com/level12/webgrid" classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", @@ -35,6 +35,53 @@ dependencies = [ [project.optional-dependencies] i18n = ["morphi"] +tests-stable = [ + "arrow==1.3.0", + "beautifulsoup4==4.12.3", + "BlazeUtils==0.7.0", + "blinker==1.8.2", + "click==8.1.7", + "coverage==7.6.0", + "cssselect==1.2.0", + "dominate==2.9.1", + "et-xmlfile==1.1.0", + "Flask==3.0.3", + "Flask-Bootstrap==3.3.7.1", + "Flask-SQLAlchemy==3.1.1", + "Flask-WebTest==0.1.4", + "Flask-WTF==1.2.1", + "greenlet==3.0.3", + "iniconfig==2.0.0", + "itsdangerous==2.2.0", + "Jinja2==3.1.4", + "lxml==5.2.2", + "MarkupSafe==2.1.5", + "mock==5.1.0", + "openpyxl==3.1.5", + "packaging==24.1", + "pluggy==1.5.0", + "psycopg2-binary==2.9.9", + "pyodbc==5.1.0", + "pyquery==2.0.0", + "pytest==8.2.2", + "pytest-cov==5.0.0", + "python-dateutil==2.9.0.post0", + "six==1.16.0", + "soupsieve==2.5", + "SQLAlchemy==2.0.31", + "sqlalchemy-pyodbc-mssql==0.1.1", + "SQLAlchemy-Utils==0.41.2", + "types-python-dateutil==2.9.0.20240316", + "typing_extensions==4.12.2", + "visitor==0.1.3", + "waitress==3.0.0", + "WebOb==1.8.7", + "WebTest==3.0.0", + "Werkzeug==3.0.3", + "wrapt==1.16.0", + "WTForms==3.1.2", + "XlsxWriter==3.2.0", +] [dependency-groups] # Note: keeping Coppy deps grouped separate from app deps should help avoid unnecessary diff --git a/stable-requirements.txt b/stable-requirements.txt deleted file mode 100644 index 8b761d0..0000000 --- a/stable-requirements.txt +++ /dev/null @@ -1,45 +0,0 @@ -arrow==1.3.0 -beautifulsoup4==4.12.3 -BlazeUtils==0.7.0 -blinker==1.8.2 -click==8.1.7 -coverage==7.6.0 -cssselect==1.2.0 -dominate==2.9.1 -et-xmlfile==1.1.0 -Flask==3.0.3 -Flask-Bootstrap==3.3.7.1 -Flask-SQLAlchemy==3.1.1 -Flask-WebTest==0.1.4 -Flask-WTF==1.2.1 -greenlet==3.0.3 -iniconfig==2.0.0 -itsdangerous==2.2.0 -Jinja2==3.1.4 -lxml==5.2.2 -MarkupSafe==2.1.5 -mock==5.1.0 -openpyxl==3.1.5 -packaging==24.1 -pluggy==1.5.0 -psycopg2-binary==2.9.9 -pyodbc==5.1.0 -pyquery==2.0.0 -pytest==8.2.2 -pytest-cov==5.0.0 -python-dateutil==2.9.0.post0 -six==1.16.0 -soupsieve==2.5 -SQLAlchemy==2.0.31 -sqlalchemy-pyodbc-mssql==0.1.1 -SQLAlchemy-Utils==0.41.2 -types-python-dateutil==2.9.0.20240316 -typing_extensions==4.12.2 -visitor==0.1.3 -waitress==3.0.0 -WebOb==1.8.7 -WebTest==3.0.0 -Werkzeug==3.0.3 -wrapt==1.16.0 -WTForms==3.1.2 -XlsxWriter==3.2.0 From f9bf8bf17d4b58dba85077366c46182f26c9ae3b Mon Sep 17 00:00:00 2001 From: Randy Syring Date: Sat, 2 Aug 2025 19:12:19 -0400 Subject: [PATCH 09/33] nox tests --- compose.yaml | 23 + mise.toml | 15 + noxfile.py | 64 + pyproject.toml | 72 +- src/webgrid_tests/conftest.py | 20 +- src/webgrid_tests/test_unit.py | 3 +- tasks/odbc-driver-install | 18 + uv.lock | 2246 ++++++++++++++++++++++++++++++++ 8 files changed, 2403 insertions(+), 58 deletions(-) create mode 100644 compose.yaml create mode 100644 noxfile.py create mode 100755 tasks/odbc-driver-install create mode 100644 uv.lock diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..622d389 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,23 @@ +services: + pg: + image: postgres:17-alpine + container_name: webgrid-pg + ports: + - '${DC_POSTGRES_HOST:-127.0.0.1}:${DC_POSTGRES_PORT:-5432}:5432' + environment: + # Ok for local dev, UNSAFE in most other applications. Don't blindly copy & paste + # without considering implications. + POSTGRES_HOST_AUTH_METHOD: trust + mssql: + image: mcr.microsoft.com/mssql/server:2019-latest + container_name: webgrid-mssql + environment: + ACCEPT_EULA: Y + SA_PASSWORD: Docker-sa-password + ports: + - '${DC_MSSQL_HOST:-127.0.0.1}:${DC_MSSQL_PORT:-1433}:1433' + healthcheck: + test: ["CMD", "/opt/mssql-tools18/bin/sqlcmd", "-C", "-S", "127.0.0.1", "-U", "sa", "-P", "Docker-sa-password", "-Q", "select 1"] + interval: 10s + timeout: 15s + retries: 5 diff --git a/mise.toml b/mise.toml index 769a432..530c63f 100644 --- a/mise.toml +++ b/mise.toml @@ -1,4 +1,6 @@ [env] +DC_MSSQL_PORT = '12001' +DC_POSTGRES_PORT = '12000' PROJECT_SLUG = '{{ config_root | basename | slugify }}' _.python.venv.path = '{% if env.UV_PROJECT_ENVIRONMENT %}{{ env.UV_PROJECT_ENVIRONMENT }}{% else %}.venv{% endif %}' @@ -27,3 +29,16 @@ run = [ 'uv sync --upgrade', 'pre-commit autoupdate', ] + +[tasks.mssql-docker-check] +description = 'Check the mssql service from inside the docker container' +run = ''' + docker compose exec mssql /opt/mssql-tools18/bin/sqlcmd -C -S 127.0.0.1 -U sa -P Docker-sa-password -Q "select 'connected' as status" +''' + +[tasks.mssql-host-check] +description = 'Check the mssql service from our host' +# Task odbc-driver-install installs sqlcmd +run = ''' + /opt/mssql-tools18/bin/sqlcmd -C -S 127.0.0.1 -U sa -P Docker-sa-password -Q "select 'connected' as status" +''' diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 0000000..9934145 --- /dev/null +++ b/noxfile.py @@ -0,0 +1,64 @@ +from pathlib import Path +from nox import Session, options, parametrize +from nox_uv import session + +options.default_venv_backend = 'uv' + +package_path = Path.cwd() +py_versions = ['3.10', '3.11', '3.12', '3.13'] + + +def pytest_run(session: Session, **env): + session.run( + 'pytest', + '-ra', + '--tb=native', + '--strict-markers', + '--cov=webgrid', + '--cov-config=.coveragerc', + '--cov-report=xml', + '--no-cov-on-fail', + f'--junit-xml={package_path}/ci/test-reports/{session.name}.pytests.xml', + 'src/webgrid_tests', + *session.posargs, + env=env, + ) + + +@session(python=py_versions, uv_groups=['tests']) +@parametrize('db', ['pg', 'sqlite']) +def tests(session: Session, db: str): + pytest_run(session, WEBTEST_DB=db) + + +@session(python=py_versions, uv_groups=['tests', 'mssql']) +def tests_mssql(session: Session): + pytest_run(session, WEBTEST_DB='mssql') + + +@session(python=[py_versions[-1]], uv_groups=['tests'], uv_no_install_project=True) +def wheel_tests(session: Session): + """ + Package the wheel, install in the venv, and then run the tests for one version of Python. + Helps ensure nothing is wrong with how we package the wheel. + """ + session.install('hatch', 'check-wheel-contents') + session.run('hatch', 'build', '--clean') + + version = session.run('hatch', 'version', silent=True).strip() + wheel_fpath = package_path / 'tmp' / 'dist' / f'webgrid-{version}-py3-none-any.whl' + + session.run('check-wheel-contents', wheel_fpath) + + session.run('uv', 'pip', 'install', wheel_fpath) + pytest_run(session) + + +@session(uv_groups=['pre-commit']) +def precommit(session: Session): + session.run( + 'pre-commit', + 'run', + '--all-files', + ) + diff --git a/pyproject.toml b/pyproject.toml index c6d820e..5414c52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,53 +35,6 @@ dependencies = [ [project.optional-dependencies] i18n = ["morphi"] -tests-stable = [ - "arrow==1.3.0", - "beautifulsoup4==4.12.3", - "BlazeUtils==0.7.0", - "blinker==1.8.2", - "click==8.1.7", - "coverage==7.6.0", - "cssselect==1.2.0", - "dominate==2.9.1", - "et-xmlfile==1.1.0", - "Flask==3.0.3", - "Flask-Bootstrap==3.3.7.1", - "Flask-SQLAlchemy==3.1.1", - "Flask-WebTest==0.1.4", - "Flask-WTF==1.2.1", - "greenlet==3.0.3", - "iniconfig==2.0.0", - "itsdangerous==2.2.0", - "Jinja2==3.1.4", - "lxml==5.2.2", - "MarkupSafe==2.1.5", - "mock==5.1.0", - "openpyxl==3.1.5", - "packaging==24.1", - "pluggy==1.5.0", - "psycopg2-binary==2.9.9", - "pyodbc==5.1.0", - "pyquery==2.0.0", - "pytest==8.2.2", - "pytest-cov==5.0.0", - "python-dateutil==2.9.0.post0", - "six==1.16.0", - "soupsieve==2.5", - "SQLAlchemy==2.0.31", - "sqlalchemy-pyodbc-mssql==0.1.1", - "SQLAlchemy-Utils==0.41.2", - "types-python-dateutil==2.9.0.20240316", - "typing_extensions==4.12.2", - "visitor==0.1.3", - "waitress==3.0.0", - "WebOb==1.8.7", - "WebTest==3.0.0", - "Werkzeug==3.0.3", - "wrapt==1.16.0", - "WTForms==3.1.2", - "XlsxWriter==3.2.0", -] [dependency-groups] # Note: keeping Coppy deps grouped separate from app deps should help avoid unnecessary @@ -95,18 +48,27 @@ dev = [ 'click', 'hatch', 'ruff', - - # App specific: - # TODO: fill in app deps here +] +mssql = [ + # NOTE: you will also need the driver, which is an OS level install. + # See: tasks/odbc-driver-install + 'pyodbc', ] # Used by nox tests = [ - # From Coppy: + "arrow>=1.3.0", + "flask>=3.0.3", + "flask-bootstrap>=3.3.7.1", + "flask-sqlalchemy>=3.1.1", + "flask-webtest>=0.1.6", + "flask-wtf>=1.2.2", + "openpyxl>=3.1.5", + "psycopg[binary]>=3.2.9", + "pyquery>=2.0.1", 'pytest', 'pytest-cov', - - # App specific: - # TODO: fill in app deps here + "sqlalchemy-utils>=0.41.2", + "xlsxwriter>=3.2.5", ] # Used by nox pre-commit = [ @@ -121,5 +83,5 @@ audit = [ ] # Used by CI nox = [ - 'nox', + 'nox-uv', ] diff --git a/src/webgrid_tests/conftest.py b/src/webgrid_tests/conftest.py index 76268f4..b1a6650 100644 --- a/src/webgrid_tests/conftest.py +++ b/src/webgrid_tests/conftest.py @@ -1,9 +1,25 @@ -import os +from os import environ +# Default URLs works for Docker compose and CI +db_kind = environ.get('WEBTEST_DB', 'pg') + +if db_kind == 'pg': + db_port = environ.get('DC_POSTGRES_PORT', '5432') + default_url = f'postgresql+psycopg://postgres@127.0.0.1:{db_port}/postgres' +elif db_kind == 'mssql': + # https://learn.microsoft.com/en-us/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server + db_port = environ.get('DC_MSSQL_PORT', '1433') + default_url = f"mssql+pyodbc://SA:Docker-sa-password@127.0.0.1:{db_port}/tempdb?driver=ODBC+Driver+18+for+SQL+Server&trustservercertificate=yes" +else: + assert db_kind == 'sqlite' + default_url = 'sqlite:///' + +db_url = environ.get('SQLALCHEMY_DATABASE_URI', default_url) +print('Webgrid tests database URL', db_url) def pytest_configure(config): from webgrid_ta.app import create_app - app = create_app(config='Test', database_url=os.environ.get('SQLALCHEMY_DATABASE_URI')) + app = create_app(config='Test', database_url=db_url) app.app_context().push() from webgrid_ta.model import load_db diff --git a/src/webgrid_tests/test_unit.py b/src/webgrid_tests/test_unit.py index e549449..906ffb7 100644 --- a/src/webgrid_tests/test_unit.py +++ b/src/webgrid_tests/test_unit.py @@ -9,7 +9,8 @@ import arrow import flask import pytest -from mock import mock, MagicMock +from unittest import mock +from unittest.mock import MagicMock import sqlalchemy.sql as sasql from werkzeug.datastructures import MultiDict diff --git a/tasks/odbc-driver-install b/tasks/odbc-driver-install new file mode 100755 index 0000000..4a812cf --- /dev/null +++ b/tasks/odbc-driver-install @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# https://learn.microsoft.com/en-us/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server +if ! [[ "18.04 20.04 22.04 24.04 24.10" == *"$(grep VERSION_ID /etc/os-release | cut -d '"' -f 2)"* ]]; +then + echo "Ubuntu $(grep VERSION_ID /etc/os-release | cut -d '"' -f 2) is not currently supported."; + exit; +fi + +# Download the package to configure the Microsoft repo +curl -sSL -O https://packages.microsoft.com/config/ubuntu/$(grep VERSION_ID /etc/os-release | cut -d '"' -f 2)/packages-microsoft-prod.deb +# Install the package +sudo dpkg -i packages-microsoft-prod.deb +# Delete the file +rm packages-microsoft-prod.deb + +# Install the driver & tools (e.g. bcp and sqlcmd) +sudo apt-get update +sudo ACCEPT_EULA=Y apt-get install -y msodbcsql18 mssql-tools18 diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..cd3f1f3 --- /dev/null +++ b/uv.lock @@ -0,0 +1,2246 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version < '3.13'", +] + +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, +] + +[[package]] +name = "argcomplete" +version = "3.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/0f/861e168fc813c56a78b35f3c30d91c6757d1fd185af1110f1aec784b35d0/argcomplete-3.6.2.tar.gz", hash = "sha256:d0519b1bc867f5f4f4713c41ad0aba73a4a5f007449716b16f385f2166dc6adf", size = 73403, upload-time = "2025-04-03T04:57:03.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/da/e42d7a9d8dd33fa775f467e4028a47936da2f01e4b0e561f9ba0d74cb0ca/argcomplete-3.6.2-py3-none-any.whl", hash = "sha256:65b3133a29ad53fb42c48cf5114752c7ab66c1c38544fdf6460f450c09b42591", size = 43708, upload-time = "2025-04-03T04:57:01.591Z" }, +] + +[[package]] +name = "arrow" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "types-python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/00/0f6e8fcdb23ea632c866620cc872729ff43ed91d284c866b515c6342b173/arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85", size = 131960, upload-time = "2023-09-30T22:11:18.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/ed/e97229a566617f2ae958a6b13e7cc0f585470eac730a73e9e82c32a3cdd2/arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80", size = 66419, upload-time = "2023-09-30T22:11:16.072Z" }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, +] + +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067, upload-time = "2025-04-15T17:05:13.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload-time = "2025-04-15T17:05:12.221Z" }, +] + +[[package]] +name = "blazeutils" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a4/50/3dc73f260b5a5272754897ebb683145ca004a620ac6af102131e734ee4b7/BlazeUtils-0.7.0.tar.gz", hash = "sha256:5075f2f277e0c5f11ad18cfc03493385811fc99a4cbceb23c9eeb3699b5d5979", size = 30859, upload-time = "2022-10-27T12:28:42.636Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/c2/f0abdfd7b13057579b03cff0c7432fc03d334adcdb819070fecb4a39ad92/BlazeUtils-0.7.0-py3-none-any.whl", hash = "sha256:3ce3f95f44a171443f9fef69bbccc159954f7cdb83ed3aebfe97f7ea1a52eb88", size = 31537, upload-time = "2022-10-27T12:28:39.831Z" }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "boolean-py" +version = "5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/cf/85379f13b76f3a69bca86b60237978af17d6aa0bc5998978c3b8cf05abb2/boolean_py-5.0.tar.gz", hash = "sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95", size = 37047, upload-time = "2025-04-03T10:39:49.734Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/ca/78d423b324b8d77900030fa59c4aa9054261ef0925631cd2501dd015b7b7/boolean_py-5.0-py3-none-any.whl", hash = "sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9", size = 26577, upload-time = "2025-04-03T10:39:48.449Z" }, +] + +[[package]] +name = "cachecontrol" +version = "0.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "msgpack" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/3a/0cbeb04ea57d2493f3ec5a069a117ab467f85e4a10017c6d854ddcbff104/cachecontrol-0.14.3.tar.gz", hash = "sha256:73e7efec4b06b20d9267b441c1f733664f989fb8688391b670ca812d70795d11", size = 28985, upload-time = "2025-04-30T16:45:06.135Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/4c/800b0607b00b3fd20f1087f80ab53d6b4d005515b0f773e4831e37cfa83f/cachecontrol-0.14.3-py3-none-any.whl", hash = "sha256:b35e44a3113f17d2a31c1e6b27b9de6d4405f84ae51baa8c1d3cc5b633010cae", size = 21802, upload-time = "2025-04-30T16:45:03.863Z" }, +] + +[package.optional-dependencies] +filecache = [ + { name = "filelock" }, +] + +[[package]] +name = "certifi" +version = "2025.7.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload-time = "2025-07-14T03:29:28.449Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191, upload-time = "2024-09-04T20:43:30.027Z" }, + { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592, upload-time = "2024-09-04T20:43:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804, upload-time = "2024-09-04T20:43:48.186Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299, upload-time = "2024-09-04T20:43:49.812Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" }, + { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" }, + { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" }, + { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" }, + { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" }, + { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" }, + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[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 = "colorlog" +version = "6.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/7a/359f4d5df2353f26172b3cc39ea32daa39af8de522205f512f458923e677/colorlog-6.9.0.tar.gz", hash = "sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2", size = 16624, upload-time = "2024-10-29T18:34:51.011Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-py3-none-any.whl", hash = "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff", size = 11424, upload-time = "2024-10-29T18:34:49.815Z" }, +] + +[[package]] +name = "coverage" +version = "7.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/0e/66dbd4c6a7f0758a8d18044c048779ba21fb94856e1edcf764bd5403e710/coverage-7.10.1.tar.gz", hash = "sha256:ae2b4856f29ddfe827106794f3589949a57da6f0d38ab01e24ec35107979ba57", size = 819938, upload-time = "2025-07-27T14:13:39.045Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/e7/0f4e35a15361337529df88151bddcac8e8f6d6fd01da94a4b7588901c2fe/coverage-7.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1c86eb388bbd609d15560e7cc0eb936c102b6f43f31cf3e58b4fd9afe28e1372", size = 214627, upload-time = "2025-07-27T14:11:01.211Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fd/17872e762c408362072c936dbf3ca28c67c609a1f5af434b1355edcb7e12/coverage-7.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6b4ba0f488c1bdb6bd9ba81da50715a372119785458831c73428a8566253b86b", size = 215015, upload-time = "2025-07-27T14:11:03.988Z" }, + { url = "https://files.pythonhosted.org/packages/54/50/c9d445ba38ee5f685f03876c0f8223469e2e46c5d3599594dca972b470c8/coverage-7.10.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:083442ecf97d434f0cb3b3e3676584443182653da08b42e965326ba12d6b5f2a", size = 241995, upload-time = "2025-07-27T14:11:05.983Z" }, + { url = "https://files.pythonhosted.org/packages/cc/83/4ae6e0f60376af33de543368394d21b9ac370dc86434039062ef171eebf8/coverage-7.10.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c1a40c486041006b135759f59189385da7c66d239bad897c994e18fd1d0c128f", size = 243253, upload-time = "2025-07-27T14:11:07.424Z" }, + { url = "https://files.pythonhosted.org/packages/49/90/17a4d9ac7171be364ce8c0bb2b6da05e618ebfe1f11238ad4f26c99f5467/coverage-7.10.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3beb76e20b28046989300c4ea81bf690df84ee98ade4dc0bbbf774a28eb98440", size = 245110, upload-time = "2025-07-27T14:11:09.152Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/edc3f485d536ed417f3af2b4969582bcb5fab456241721825fa09354161e/coverage-7.10.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bc265a7945e8d08da28999ad02b544963f813a00f3ed0a7a0ce4165fd77629f8", size = 243056, upload-time = "2025-07-27T14:11:10.586Z" }, + { url = "https://files.pythonhosted.org/packages/58/2c/c4c316a57718556b8d0cc8304437741c31b54a62934e7c8c551a7915c2f4/coverage-7.10.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:47c91f32ba4ac46f1e224a7ebf3f98b4b24335bad16137737fe71a5961a0665c", size = 241731, upload-time = "2025-07-27T14:11:12.145Z" }, + { url = "https://files.pythonhosted.org/packages/f7/93/c78e144c6f086043d0d7d9237c5b880e71ac672ed2712c6f8cca5544481f/coverage-7.10.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1a108dd78ed185020f66f131c60078f3fae3f61646c28c8bb4edd3fa121fc7fc", size = 242023, upload-time = "2025-07-27T14:11:13.573Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e1/34e8505ca81fc144a612e1cc79fadd4a78f42e96723875f4e9f1f470437e/coverage-7.10.1-cp310-cp310-win32.whl", hash = "sha256:7092cc82382e634075cc0255b0b69cb7cada7c1f249070ace6a95cb0f13548ef", size = 217130, upload-time = "2025-07-27T14:11:15.11Z" }, + { url = "https://files.pythonhosted.org/packages/75/2b/82adfce6edffc13d804aee414e64c0469044234af9296e75f6d13f92f6a2/coverage-7.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:ac0c5bba938879c2fc0bc6c1b47311b5ad1212a9dcb8b40fe2c8110239b7faed", size = 218015, upload-time = "2025-07-27T14:11:16.836Z" }, + { url = "https://files.pythonhosted.org/packages/20/8e/ef088112bd1b26e2aa931ee186992b3e42c222c64f33e381432c8ee52aae/coverage-7.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b45e2f9d5b0b5c1977cb4feb5f594be60eb121106f8900348e29331f553a726f", size = 214747, upload-time = "2025-07-27T14:11:18.217Z" }, + { url = "https://files.pythonhosted.org/packages/2d/76/a1e46f3c6e0897758eb43af88bb3c763cb005f4950769f7b553e22aa5f89/coverage-7.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a7a4d74cb0f5e3334f9aa26af7016ddb94fb4bfa11b4a573d8e98ecba8c34f1", size = 215128, upload-time = "2025-07-27T14:11:19.706Z" }, + { url = "https://files.pythonhosted.org/packages/78/4d/903bafb371a8c887826ecc30d3977b65dfad0e1e66aa61b7e173de0828b0/coverage-7.10.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d4b0aab55ad60ead26159ff12b538c85fbab731a5e3411c642b46c3525863437", size = 245140, upload-time = "2025-07-27T14:11:21.261Z" }, + { url = "https://files.pythonhosted.org/packages/55/f1/1f8f09536f38394a8698dd08a0e9608a512eacee1d3b771e2d06397f77bf/coverage-7.10.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dcc93488c9ebd229be6ee1f0d9aad90da97b33ad7e2912f5495804d78a3cd6b7", size = 246977, upload-time = "2025-07-27T14:11:23.15Z" }, + { url = "https://files.pythonhosted.org/packages/57/cc/ed6bbc5a3bdb36ae1bca900bbbfdcb23b260ef2767a7b2dab38b92f61adf/coverage-7.10.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa309df995d020f3438407081b51ff527171cca6772b33cf8f85344b8b4b8770", size = 249140, upload-time = "2025-07-27T14:11:24.743Z" }, + { url = "https://files.pythonhosted.org/packages/10/f5/e881ade2d8e291b60fa1d93d6d736107e940144d80d21a0d4999cff3642f/coverage-7.10.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cfb8b9d8855c8608f9747602a48ab525b1d320ecf0113994f6df23160af68262", size = 246869, upload-time = "2025-07-27T14:11:26.156Z" }, + { url = "https://files.pythonhosted.org/packages/53/b9/6a5665cb8996e3cd341d184bb11e2a8edf01d8dadcf44eb1e742186cf243/coverage-7.10.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:320d86da829b012982b414c7cdda65f5d358d63f764e0e4e54b33097646f39a3", size = 244899, upload-time = "2025-07-27T14:11:27.622Z" }, + { url = "https://files.pythonhosted.org/packages/27/11/24156776709c4e25bf8a33d6bb2ece9a9067186ddac19990f6560a7f8130/coverage-7.10.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dc60ddd483c556590da1d9482a4518292eec36dd0e1e8496966759a1f282bcd0", size = 245507, upload-time = "2025-07-27T14:11:29.544Z" }, + { url = "https://files.pythonhosted.org/packages/43/db/a6f0340b7d6802a79928659c9a32bc778ea420e87a61b568d68ac36d45a8/coverage-7.10.1-cp311-cp311-win32.whl", hash = "sha256:4fcfe294f95b44e4754da5b58be750396f2b1caca8f9a0e78588e3ef85f8b8be", size = 217167, upload-time = "2025-07-27T14:11:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6f/1990eb4fd05cea4cfabdf1d587a997ac5f9a8bee883443a1d519a2a848c9/coverage-7.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:efa23166da3fe2915f8ab452dde40319ac84dc357f635737174a08dbd912980c", size = 218054, upload-time = "2025-07-27T14:11:33.202Z" }, + { url = "https://files.pythonhosted.org/packages/b4/4d/5e061d6020251b20e9b4303bb0b7900083a1a384ec4e5db326336c1c4abd/coverage-7.10.1-cp311-cp311-win_arm64.whl", hash = "sha256:d12b15a8c3759e2bb580ffa423ae54be4f184cf23beffcbd641f4fe6e1584293", size = 216483, upload-time = "2025-07-27T14:11:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3f/b051feeb292400bd22d071fdf933b3ad389a8cef5c80c7866ed0c7414b9e/coverage-7.10.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6b7dc7f0a75a7eaa4584e5843c873c561b12602439d2351ee28c7478186c4da4", size = 214934, upload-time = "2025-07-27T14:11:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e4/a61b27d5c4c2d185bdfb0bfe9d15ab4ac4f0073032665544507429ae60eb/coverage-7.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:607f82389f0ecafc565813aa201a5cade04f897603750028dd660fb01797265e", size = 215173, upload-time = "2025-07-27T14:11:38.005Z" }, + { url = "https://files.pythonhosted.org/packages/8a/01/40a6ee05b60d02d0bc53742ad4966e39dccd450aafb48c535a64390a3552/coverage-7.10.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f7da31a1ba31f1c1d4d5044b7c5813878adae1f3af8f4052d679cc493c7328f4", size = 246190, upload-time = "2025-07-27T14:11:39.887Z" }, + { url = "https://files.pythonhosted.org/packages/11/ef/a28d64d702eb583c377255047281305dc5a5cfbfb0ee36e721f78255adb6/coverage-7.10.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51fe93f3fe4f5d8483d51072fddc65e717a175490804e1942c975a68e04bf97a", size = 248618, upload-time = "2025-07-27T14:11:41.841Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ad/73d018bb0c8317725370c79d69b5c6e0257df84a3b9b781bda27a438a3be/coverage-7.10.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e59d00830da411a1feef6ac828b90bbf74c9b6a8e87b8ca37964925bba76dbe", size = 250081, upload-time = "2025-07-27T14:11:43.705Z" }, + { url = "https://files.pythonhosted.org/packages/2d/dd/496adfbbb4503ebca5d5b2de8bed5ec00c0a76558ffc5b834fd404166bc9/coverage-7.10.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:924563481c27941229cb4e16eefacc35da28563e80791b3ddc5597b062a5c386", size = 247990, upload-time = "2025-07-27T14:11:45.244Z" }, + { url = "https://files.pythonhosted.org/packages/18/3c/a9331a7982facfac0d98a4a87b36ae666fe4257d0f00961a3a9ef73e015d/coverage-7.10.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ca79146ee421b259f8131f153102220b84d1a5e6fb9c8aed13b3badfd1796de6", size = 246191, upload-time = "2025-07-27T14:11:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/62/0c/75345895013b83f7afe92ec595e15a9a525ede17491677ceebb2ba5c3d85/coverage-7.10.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2b225a06d227f23f386fdc0eab471506d9e644be699424814acc7d114595495f", size = 247400, upload-time = "2025-07-27T14:11:48.643Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a9/98b268cfc5619ef9df1d5d34fee408ecb1542d9fd43d467e5c2f28668cd4/coverage-7.10.1-cp312-cp312-win32.whl", hash = "sha256:5ba9a8770effec5baaaab1567be916c87d8eea0c9ad11253722d86874d885eca", size = 217338, upload-time = "2025-07-27T14:11:50.258Z" }, + { url = "https://files.pythonhosted.org/packages/fe/31/22a5440e4d1451f253c5cd69fdcead65e92ef08cd4ec237b8756dc0b20a7/coverage-7.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:9eb245a8d8dd0ad73b4062135a251ec55086fbc2c42e0eb9725a9b553fba18a3", size = 218125, upload-time = "2025-07-27T14:11:52.034Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2b/40d9f0ce7ee839f08a43c5bfc9d05cec28aaa7c9785837247f96cbe490b9/coverage-7.10.1-cp312-cp312-win_arm64.whl", hash = "sha256:7718060dd4434cc719803a5e526838a5d66e4efa5dc46d2b25c21965a9c6fcc4", size = 216523, upload-time = "2025-07-27T14:11:53.965Z" }, + { url = "https://files.pythonhosted.org/packages/ef/72/135ff5fef09b1ffe78dbe6fcf1e16b2e564cd35faeacf3d63d60d887f12d/coverage-7.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ebb08d0867c5a25dffa4823377292a0ffd7aaafb218b5d4e2e106378b1061e39", size = 214960, upload-time = "2025-07-27T14:11:55.959Z" }, + { url = "https://files.pythonhosted.org/packages/b1/aa/73a5d1a6fc08ca709a8177825616aa95ee6bf34d522517c2595484a3e6c9/coverage-7.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f32a95a83c2e17422f67af922a89422cd24c6fa94041f083dd0bb4f6057d0bc7", size = 215220, upload-time = "2025-07-27T14:11:57.899Z" }, + { url = "https://files.pythonhosted.org/packages/8d/40/3124fdd45ed3772a42fc73ca41c091699b38a2c3bd4f9cb564162378e8b6/coverage-7.10.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c4c746d11c8aba4b9f58ca8bfc6fbfd0da4efe7960ae5540d1a1b13655ee8892", size = 245772, upload-time = "2025-07-27T14:12:00.422Z" }, + { url = "https://files.pythonhosted.org/packages/42/62/a77b254822efa8c12ad59e8039f2bc3df56dc162ebda55e1943e35ba31a5/coverage-7.10.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7f39edd52c23e5c7ed94e0e4bf088928029edf86ef10b95413e5ea670c5e92d7", size = 248116, upload-time = "2025-07-27T14:12:03.099Z" }, + { url = "https://files.pythonhosted.org/packages/1d/01/8101f062f472a3a6205b458d18ef0444a63ae5d36a8a5ed5dd0f6167f4db/coverage-7.10.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab6e19b684981d0cd968906e293d5628e89faacb27977c92f3600b201926b994", size = 249554, upload-time = "2025-07-27T14:12:04.668Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7b/e51bc61573e71ff7275a4f167aecbd16cb010aefdf54bcd8b0a133391263/coverage-7.10.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5121d8cf0eacb16133501455d216bb5f99899ae2f52d394fe45d59229e6611d0", size = 247766, upload-time = "2025-07-27T14:12:06.234Z" }, + { url = "https://files.pythonhosted.org/packages/4b/71/1c96d66a51d4204a9d6d12df53c4071d87e110941a2a1fe94693192262f5/coverage-7.10.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df1c742ca6f46a6f6cbcaef9ac694dc2cb1260d30a6a2f5c68c5f5bcfee1cfd7", size = 245735, upload-time = "2025-07-27T14:12:08.305Z" }, + { url = "https://files.pythonhosted.org/packages/13/d5/efbc2ac4d35ae2f22ef6df2ca084c60e13bd9378be68655e3268c80349ab/coverage-7.10.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:40f9a38676f9c073bf4b9194707aa1eb97dca0e22cc3766d83879d72500132c7", size = 247118, upload-time = "2025-07-27T14:12:09.903Z" }, + { url = "https://files.pythonhosted.org/packages/d1/22/073848352bec28ca65f2b6816b892fcf9a31abbef07b868487ad15dd55f1/coverage-7.10.1-cp313-cp313-win32.whl", hash = "sha256:2348631f049e884839553b9974f0821d39241c6ffb01a418efce434f7eba0fe7", size = 217381, upload-time = "2025-07-27T14:12:11.535Z" }, + { url = "https://files.pythonhosted.org/packages/b7/df/df6a0ff33b042f000089bd11b6bb034bab073e2ab64a56e78ed882cba55d/coverage-7.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:4072b31361b0d6d23f750c524f694e1a417c1220a30d3ef02741eed28520c48e", size = 218152, upload-time = "2025-07-27T14:12:13.182Z" }, + { url = "https://files.pythonhosted.org/packages/30/e3/5085ca849a40ed6b47cdb8f65471c2f754e19390b5a12fa8abd25cbfaa8f/coverage-7.10.1-cp313-cp313-win_arm64.whl", hash = "sha256:3e31dfb8271937cab9425f19259b1b1d1f556790e98eb266009e7a61d337b6d4", size = 216559, upload-time = "2025-07-27T14:12:14.807Z" }, + { url = "https://files.pythonhosted.org/packages/cc/93/58714efbfdeb547909feaabe1d67b2bdd59f0597060271b9c548d5efb529/coverage-7.10.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1c4f679c6b573a5257af6012f167a45be4c749c9925fd44d5178fd641ad8bf72", size = 215677, upload-time = "2025-07-27T14:12:16.68Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0c/18eaa5897e7e8cb3f8c45e563e23e8a85686b4585e29d53cacb6bc9cb340/coverage-7.10.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:871ebe8143da284bd77b84a9136200bd638be253618765d21a1fce71006d94af", size = 215899, upload-time = "2025-07-27T14:12:18.758Z" }, + { url = "https://files.pythonhosted.org/packages/84/c1/9d1affacc3c75b5a184c140377701bbf14fc94619367f07a269cd9e4fed6/coverage-7.10.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:998c4751dabf7d29b30594af416e4bf5091f11f92a8d88eb1512c7ba136d1ed7", size = 257140, upload-time = "2025-07-27T14:12:20.357Z" }, + { url = "https://files.pythonhosted.org/packages/3d/0f/339bc6b8fa968c346df346068cca1f24bdea2ddfa93bb3dc2e7749730962/coverage-7.10.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:780f750a25e7749d0af6b3631759c2c14f45de209f3faaa2398312d1c7a22759", size = 259005, upload-time = "2025-07-27T14:12:22.007Z" }, + { url = "https://files.pythonhosted.org/packages/c8/22/89390864b92ea7c909079939b71baba7e5b42a76bf327c1d615bd829ba57/coverage-7.10.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:590bdba9445df4763bdbebc928d8182f094c1f3947a8dc0fc82ef014dbdd8324", size = 261143, upload-time = "2025-07-27T14:12:23.746Z" }, + { url = "https://files.pythonhosted.org/packages/2c/56/3d04d89017c0c41c7a71bd69b29699d919b6bbf2649b8b2091240b97dd6a/coverage-7.10.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b2df80cb6a2af86d300e70acb82e9b79dab2c1e6971e44b78dbfc1a1e736b53", size = 258735, upload-time = "2025-07-27T14:12:25.73Z" }, + { url = "https://files.pythonhosted.org/packages/cb/40/312252c8afa5ca781063a09d931f4b9409dc91526cd0b5a2b84143ffafa2/coverage-7.10.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d6a558c2725bfb6337bf57c1cd366c13798bfd3bfc9e3dd1f4a6f6fc95a4605f", size = 256871, upload-time = "2025-07-27T14:12:27.767Z" }, + { url = "https://files.pythonhosted.org/packages/1f/2b/564947d5dede068215aaddb9e05638aeac079685101462218229ddea9113/coverage-7.10.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e6150d167f32f2a54690e572e0a4c90296fb000a18e9b26ab81a6489e24e78dd", size = 257692, upload-time = "2025-07-27T14:12:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/93/1b/c8a867ade85cb26d802aea2209b9c2c80613b9c122baa8c8ecea6799648f/coverage-7.10.1-cp313-cp313t-win32.whl", hash = "sha256:d946a0c067aa88be4a593aad1236493313bafaa27e2a2080bfe88db827972f3c", size = 218059, upload-time = "2025-07-27T14:12:31.076Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fe/cd4ab40570ae83a516bf5e754ea4388aeedd48e660e40c50b7713ed4f930/coverage-7.10.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e37c72eaccdd5ed1130c67a92ad38f5b2af66eeff7b0abe29534225db2ef7b18", size = 219150, upload-time = "2025-07-27T14:12:32.746Z" }, + { url = "https://files.pythonhosted.org/packages/8d/16/6e5ed5854be6d70d0c39e9cb9dd2449f2c8c34455534c32c1a508c7dbdb5/coverage-7.10.1-cp313-cp313t-win_arm64.whl", hash = "sha256:89ec0ffc215c590c732918c95cd02b55c7d0f569d76b90bb1a5e78aa340618e4", size = 217014, upload-time = "2025-07-27T14:12:34.406Z" }, + { url = "https://files.pythonhosted.org/packages/54/8e/6d0bfe9c3d7121cf936c5f8b03e8c3da1484fb801703127dba20fb8bd3c7/coverage-7.10.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:166d89c57e877e93d8827dac32cedae6b0277ca684c6511497311249f35a280c", size = 214951, upload-time = "2025-07-27T14:12:36.069Z" }, + { url = "https://files.pythonhosted.org/packages/f2/29/e3e51a8c653cf2174c60532aafeb5065cea0911403fa144c9abe39790308/coverage-7.10.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bed4a2341b33cd1a7d9ffc47df4a78ee61d3416d43b4adc9e18b7d266650b83e", size = 215229, upload-time = "2025-07-27T14:12:37.759Z" }, + { url = "https://files.pythonhosted.org/packages/e0/59/3c972080b2fa18b6c4510201f6d4dc87159d450627d062cd9ad051134062/coverage-7.10.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ddca1e4f5f4c67980533df01430184c19b5359900e080248bbf4ed6789584d8b", size = 245738, upload-time = "2025-07-27T14:12:39.453Z" }, + { url = "https://files.pythonhosted.org/packages/2e/04/fc0d99d3f809452654e958e1788454f6e27b34e43f8f8598191c8ad13537/coverage-7.10.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:37b69226001d8b7de7126cad7366b0778d36777e4d788c66991455ba817c5b41", size = 248045, upload-time = "2025-07-27T14:12:41.387Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2e/afcbf599e77e0dfbf4c97197747250d13d397d27e185b93987d9eaac053d/coverage-7.10.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2f22102197bcb1722691296f9e589f02b616f874e54a209284dd7b9294b0b7f", size = 249666, upload-time = "2025-07-27T14:12:43.056Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ae/bc47f7f8ecb7a06cbae2bf86a6fa20f479dd902bc80f57cff7730438059d/coverage-7.10.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1e0c768b0f9ac5839dac5cf88992a4bb459e488ee8a1f8489af4cb33b1af00f1", size = 247692, upload-time = "2025-07-27T14:12:44.83Z" }, + { url = "https://files.pythonhosted.org/packages/b6/26/cbfa3092d31ccba8ba7647e4d25753263e818b4547eba446b113d7d1efdf/coverage-7.10.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:991196702d5e0b120a8fef2664e1b9c333a81d36d5f6bcf6b225c0cf8b0451a2", size = 245536, upload-time = "2025-07-27T14:12:46.527Z" }, + { url = "https://files.pythonhosted.org/packages/56/77/9c68e92500e6a1c83d024a70eadcc9a173f21aadd73c4675fe64c9c43fdf/coverage-7.10.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae8e59e5f4fd85d6ad34c2bb9d74037b5b11be072b8b7e9986beb11f957573d4", size = 246954, upload-time = "2025-07-27T14:12:49.279Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a5/ba96671c5a669672aacd9877a5987c8551501b602827b4e84256da2a30a7/coverage-7.10.1-cp314-cp314-win32.whl", hash = "sha256:042125c89cf74a074984002e165d61fe0e31c7bd40ebb4bbebf07939b5924613", size = 217616, upload-time = "2025-07-27T14:12:51.214Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3c/e1e1eb95fc1585f15a410208c4795db24a948e04d9bde818fe4eb893bc85/coverage-7.10.1-cp314-cp314-win_amd64.whl", hash = "sha256:a22c3bfe09f7a530e2c94c87ff7af867259c91bef87ed2089cd69b783af7b84e", size = 218412, upload-time = "2025-07-27T14:12:53.429Z" }, + { url = "https://files.pythonhosted.org/packages/b0/85/7e1e5be2cb966cba95566ba702b13a572ca744fbb3779df9888213762d67/coverage-7.10.1-cp314-cp314-win_arm64.whl", hash = "sha256:ee6be07af68d9c4fca4027c70cea0c31a0f1bc9cb464ff3c84a1f916bf82e652", size = 216776, upload-time = "2025-07-27T14:12:55.482Z" }, + { url = "https://files.pythonhosted.org/packages/62/0f/5bb8f29923141cca8560fe2217679caf4e0db643872c1945ac7d8748c2a7/coverage-7.10.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d24fb3c0c8ff0d517c5ca5de7cf3994a4cd559cde0315201511dbfa7ab528894", size = 215698, upload-time = "2025-07-27T14:12:57.225Z" }, + { url = "https://files.pythonhosted.org/packages/80/29/547038ffa4e8e4d9e82f7dfc6d152f75fcdc0af146913f0ba03875211f03/coverage-7.10.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1217a54cfd79be20512a67ca81c7da3f2163f51bbfd188aab91054df012154f5", size = 215902, upload-time = "2025-07-27T14:12:59.071Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8a/7aaa8fbfaed900147987a424e112af2e7790e1ac9cd92601e5bd4e1ba60a/coverage-7.10.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:51f30da7a52c009667e02f125737229d7d8044ad84b79db454308033a7808ab2", size = 257230, upload-time = "2025-07-27T14:13:01.248Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1d/c252b5ffac44294e23a0d79dd5acf51749b39795ccc898faeabf7bee903f/coverage-7.10.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ed3718c757c82d920f1c94089066225ca2ad7f00bb904cb72b1c39ebdd906ccb", size = 259194, upload-time = "2025-07-27T14:13:03.247Z" }, + { url = "https://files.pythonhosted.org/packages/16/ad/6c8d9f83d08f3bac2e7507534d0c48d1a4f52c18e6f94919d364edbdfa8f/coverage-7.10.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc452481e124a819ced0c25412ea2e144269ef2f2534b862d9f6a9dae4bda17b", size = 261316, upload-time = "2025-07-27T14:13:04.957Z" }, + { url = "https://files.pythonhosted.org/packages/d6/4e/f9bbf3a36c061e2e0e0f78369c006d66416561a33d2bee63345aee8ee65e/coverage-7.10.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9d6f494c307e5cb9b1e052ec1a471060f1dea092c8116e642e7a23e79d9388ea", size = 258794, upload-time = "2025-07-27T14:13:06.715Z" }, + { url = "https://files.pythonhosted.org/packages/87/82/e600bbe78eb2cb0541751d03cef9314bcd0897e8eea156219c39b685f869/coverage-7.10.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fc0e46d86905ddd16b85991f1f4919028092b4e511689bbdaff0876bd8aab3dd", size = 256869, upload-time = "2025-07-27T14:13:08.933Z" }, + { url = "https://files.pythonhosted.org/packages/ce/5d/2fc9a9236c5268f68ac011d97cd3a5ad16cc420535369bedbda659fdd9b7/coverage-7.10.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80b9ccd82e30038b61fc9a692a8dc4801504689651b281ed9109f10cc9fe8b4d", size = 257765, upload-time = "2025-07-27T14:13:10.778Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/b4e00b2bd48a2dc8e1c7d2aea7455f40af2e36484ab2ef06deb85883e9fe/coverage-7.10.1-cp314-cp314t-win32.whl", hash = "sha256:e58991a2b213417285ec866d3cd32db17a6a88061a985dbb7e8e8f13af429c47", size = 218420, upload-time = "2025-07-27T14:13:12.882Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d21d05f33ea27ece327422240e69654b5932b0b29e7fbc40fbab3cf199bf/coverage-7.10.1-cp314-cp314t-win_amd64.whl", hash = "sha256:e88dd71e4ecbc49d9d57d064117462c43f40a21a1383507811cf834a4a620651", size = 219536, upload-time = "2025-07-27T14:13:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/a6/68/7fea94b141281ed8be3d1d5c4319a97f2befc3e487ce33657fc64db2c45e/coverage-7.10.1-cp314-cp314t-win_arm64.whl", hash = "sha256:1aadfb06a30c62c2eb82322171fe1f7c288c80ca4156d46af0ca039052814bab", size = 217190, upload-time = "2025-07-27T14:13:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/0f/64/922899cff2c0fd3496be83fa8b81230f5a8d82a2ad30f98370b133c2c83b/coverage-7.10.1-py3-none-any.whl", hash = "sha256:fa2a258aa6bf188eb9a8948f7102a83da7c430a0dce918dbd8b60ef8fcb772d7", size = 206597, upload-time = "2025-07-27T14:13:37.221Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "45.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/1e/49527ac611af559665f71cbb8f92b332b5ec9c6fbc4e88b0f8e92f5e85df/cryptography-45.0.5.tar.gz", hash = "sha256:72e76caa004ab63accdf26023fccd1d087f6d90ec6048ff33ad0445abf7f605a", size = 744903, upload-time = "2025-07-02T13:06:25.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/05/2194432935e29b91fb649f6149c1a4f9e6d3d9fc880919f4ad1bcc22641e/cryptography-45.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3a264aae5f7fbb089dbc01e0242d3b67dffe3e6292e1f5182122bdf58e65215d", size = 4205926, upload-time = "2025-07-02T13:05:04.741Z" }, + { url = "https://files.pythonhosted.org/packages/07/8b/9ef5da82350175e32de245646b1884fc01124f53eb31164c77f95a08d682/cryptography-45.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e74d30ec9c7cb2f404af331d5b4099a9b322a8a6b25c4632755c8757345baac5", size = 4429235, upload-time = "2025-07-02T13:05:07.084Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e1/c809f398adde1994ee53438912192d92a1d0fc0f2d7582659d9ef4c28b0c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3af26738f2db354aafe492fb3869e955b12b2ef2e16908c8b9cb928128d42c57", size = 4209785, upload-time = "2025-07-02T13:05:09.321Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8b/07eb6bd5acff58406c5e806eff34a124936f41a4fb52909ffa4d00815f8c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e6c00130ed423201c5bc5544c23359141660b07999ad82e34e7bb8f882bb78e0", size = 3893050, upload-time = "2025-07-02T13:05:11.069Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ef/3333295ed58d900a13c92806b67e62f27876845a9a908c939f040887cca9/cryptography-45.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:dd420e577921c8c2d31289536c386aaa30140b473835e97f83bc71ea9d2baf2d", size = 4457379, upload-time = "2025-07-02T13:05:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9d/44080674dee514dbb82b21d6fa5d1055368f208304e2ab1828d85c9de8f4/cryptography-45.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d05a38884db2ba215218745f0781775806bde4f32e07b135348355fe8e4991d9", size = 4209355, upload-time = "2025-07-02T13:05:15.017Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d8/0749f7d39f53f8258e5c18a93131919ac465ee1f9dccaf1b3f420235e0b5/cryptography-45.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ad0caded895a00261a5b4aa9af828baede54638754b51955a0ac75576b831b27", size = 4456087, upload-time = "2025-07-02T13:05:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/09/d7/92acac187387bf08902b0bf0699816f08553927bdd6ba3654da0010289b4/cryptography-45.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9024beb59aca9d31d36fcdc1604dd9bbeed0a55bface9f1908df19178e2f116e", size = 4332873, upload-time = "2025-07-02T13:05:18.743Z" }, + { url = "https://files.pythonhosted.org/packages/03/c2/840e0710da5106a7c3d4153c7215b2736151bba60bf4491bdb421df5056d/cryptography-45.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91098f02ca81579c85f66df8a588c78f331ca19089763d733e34ad359f474174", size = 4564651, upload-time = "2025-07-02T13:05:21.382Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e7/2187be2f871c0221a81f55ee3105d3cf3e273c0a0853651d7011eada0d7e/cryptography-45.0.5-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3fcfbefc4a7f332dece7272a88e410f611e79458fab97b5efe14e54fe476f4fd", size = 4197780, upload-time = "2025-07-02T13:05:29.299Z" }, + { url = "https://files.pythonhosted.org/packages/b9/cf/84210c447c06104e6be9122661159ad4ce7a8190011669afceeaea150524/cryptography-45.0.5-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:460f8c39ba66af7db0545a8c6f2eabcbc5a5528fc1cf6c3fa9a1e44cec33385e", size = 4420091, upload-time = "2025-07-02T13:05:31.221Z" }, + { url = "https://files.pythonhosted.org/packages/3e/6a/cb8b5c8bb82fafffa23aeff8d3a39822593cee6e2f16c5ca5c2ecca344f7/cryptography-45.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9b4cf6318915dccfe218e69bbec417fdd7c7185aa7aab139a2c0beb7468c89f0", size = 4198711, upload-time = "2025-07-02T13:05:33.062Z" }, + { url = "https://files.pythonhosted.org/packages/04/f7/36d2d69df69c94cbb2473871926daf0f01ad8e00fe3986ac3c1e8c4ca4b3/cryptography-45.0.5-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2089cc8f70a6e454601525e5bf2779e665d7865af002a5dec8d14e561002e135", size = 3883299, upload-time = "2025-07-02T13:05:34.94Z" }, + { url = "https://files.pythonhosted.org/packages/82/c7/f0ea40f016de72f81288e9fe8d1f6748036cb5ba6118774317a3ffc6022d/cryptography-45.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0027d566d65a38497bc37e0dd7c2f8ceda73597d2ac9ba93810204f56f52ebc7", size = 4450558, upload-time = "2025-07-02T13:05:37.288Z" }, + { url = "https://files.pythonhosted.org/packages/06/ae/94b504dc1a3cdf642d710407c62e86296f7da9e66f27ab12a1ee6fdf005b/cryptography-45.0.5-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:be97d3a19c16a9be00edf79dca949c8fa7eff621763666a145f9f9535a5d7f42", size = 4198020, upload-time = "2025-07-02T13:05:39.102Z" }, + { url = "https://files.pythonhosted.org/packages/05/2b/aaf0adb845d5dabb43480f18f7ca72e94f92c280aa983ddbd0bcd6ecd037/cryptography-45.0.5-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:7760c1c2e1a7084153a0f68fab76e754083b126a47d0117c9ed15e69e2103492", size = 4449759, upload-time = "2025-07-02T13:05:41.398Z" }, + { url = "https://files.pythonhosted.org/packages/91/e4/f17e02066de63e0100a3a01b56f8f1016973a1d67551beaf585157a86b3f/cryptography-45.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ff8728d8d890b3dda5765276d1bc6fb099252915a2cd3aff960c4c195745dd0", size = 4319991, upload-time = "2025-07-02T13:05:43.64Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2e/e2dbd629481b499b14516eed933f3276eb3239f7cee2dcfa4ee6b44d4711/cryptography-45.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7259038202a47fdecee7e62e0fd0b0738b6daa335354396c6ddebdbe1206af2a", size = 4554189, upload-time = "2025-07-02T13:05:46.045Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5d/a19441c1e89afb0f173ac13178606ca6fab0d3bd3ebc29e9ed1318b507fc/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c648025b6840fe62e57107e0a25f604db740e728bd67da4f6f060f03017d5097", size = 4140906, upload-time = "2025-07-02T13:05:55.914Z" }, + { url = "https://files.pythonhosted.org/packages/4b/db/daceb259982a3c2da4e619f45b5bfdec0e922a23de213b2636e78ef0919b/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b8fa8b0a35a9982a3c60ec79905ba5bb090fc0b9addcfd3dc2dd04267e45f25e", size = 4374411, upload-time = "2025-07-02T13:05:57.814Z" }, + { url = "https://files.pythonhosted.org/packages/6a/35/5d06ad06402fc522c8bf7eab73422d05e789b4e38fe3206a85e3d6966c11/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:14d96584701a887763384f3c47f0ca7c1cce322aa1c31172680eb596b890ec30", size = 4140942, upload-time = "2025-07-02T13:06:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/65/79/020a5413347e44c382ef1f7f7e7a66817cd6273e3e6b5a72d18177b08b2f/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57c816dfbd1659a367831baca4b775b2a5b43c003daf52e9d57e1d30bc2e1b0e", size = 4374079, upload-time = "2025-07-02T13:06:02.043Z" }, + { url = "https://files.pythonhosted.org/packages/f0/63/83516cfb87f4a8756eaa4203f93b283fda23d210fc14e1e594bd5f20edb6/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bd4c45986472694e5121084c6ebbd112aa919a25e783b87eb95953c9573906d6", size = 4152447, upload-time = "2025-07-02T13:06:08.345Z" }, + { url = "https://files.pythonhosted.org/packages/22/11/d2823d2a5a0bd5802b3565437add16f5c8ce1f0778bf3822f89ad2740a38/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:982518cd64c54fcada9d7e5cf28eabd3ee76bd03ab18e08a48cad7e8b6f31b18", size = 4386778, upload-time = "2025-07-02T13:06:10.263Z" }, + { url = "https://files.pythonhosted.org/packages/5f/38/6bf177ca6bce4fe14704ab3e93627c5b0ca05242261a2e43ef3168472540/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:12e55281d993a793b0e883066f590c1ae1e802e3acb67f8b442e721e475e6463", size = 4151627, upload-time = "2025-07-02T13:06:13.097Z" }, + { url = "https://files.pythonhosted.org/packages/38/6a/69fc67e5266bff68a91bcb81dff8fb0aba4d79a78521a08812048913e16f/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:5aa1e32983d4443e310f726ee4b071ab7569f58eedfdd65e9675484a4eb67bd1", size = 4385593, upload-time = "2025-07-02T13:06:15.689Z" }, +] + +[[package]] +name = "cssselect" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/0a/c3ea9573b1dc2e151abfe88c7fe0c26d1892fe6ed02d0cdb30f0d57029d5/cssselect-1.3.0.tar.gz", hash = "sha256:57f8a99424cfab289a1b6a816a43075a4b00948c86b4dcf3ef4ee7e15f7ab0c7", size = 42870, upload-time = "2025-03-10T09:30:29.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/58/257350f7db99b4ae12b614a36256d9cc870d71d9e451e79c2dc3b23d7c3c/cssselect-1.3.0-py3-none-any.whl", hash = "sha256:56d1bf3e198080cc1667e137bc51de9cadfca259f03c2d4e09037b3e01e30f0d", size = 18786, upload-time = "2025-03-10T09:30:28.048Z" }, +] + +[[package]] +name = "cyclonedx-python-lib" +version = "9.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "license-expression" }, + { name = "packageurl-python" }, + { name = "py-serializable" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/fc/abaad5482f7b59c9a0a9d8f354ce4ce23346d582a0d85730b559562bbeb4/cyclonedx_python_lib-9.1.0.tar.gz", hash = "sha256:86935f2c88a7b47a529b93c724dbd3e903bc573f6f8bd977628a7ca1b5dadea1", size = 1048735, upload-time = "2025-02-27T17:23:40.367Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/f1/f3be2e9820a2c26fa77622223e91f9c504e1581830930d477e06146073f4/cyclonedx_python_lib-9.1.0-py3-none-any.whl", hash = "sha256:55693fca8edaecc3363b24af14e82cc6e659eb1e8353e58b587c42652ce0fb52", size = 374968, upload-time = "2025-02-27T17:23:37.766Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "dependency-groups" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/55/f054de99871e7beb81935dea8a10b90cd5ce42122b1c3081d5282fdb3621/dependency_groups-1.3.1.tar.gz", hash = "sha256:78078301090517fd938c19f64a53ce98c32834dfe0dee6b88004a569a6adfefd", size = 10093, upload-time = "2025-05-02T00:34:29.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/c7/d1ec24fb280caa5a79b6b950db565dab30210a66259d17d5bb2b3a9f878d/dependency_groups-1.3.1-py3-none-any.whl", hash = "sha256:51aeaa0dfad72430fcfb7bcdbefbd75f3792e5919563077f30bc0d73f4493030", size = 8664, upload-time = "2025-05-02T00:34:27.085Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "dominate" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/f3/1c8088ff19a0fcd9c3234802a0ee47006ea64bd8852f1019194f0e3583ff/dominate-2.9.1.tar.gz", hash = "sha256:558284687d9b8aae1904e3d6051ad132dd4a8c0cf551b37ea4e7e42a31d19dc4", size = 37715, upload-time = "2023-12-24T20:45:19.192Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/19/0380af745f151a1648657bbcef0fb49ac28bf09083d94498163ffd9b32dc/dominate-2.9.1-py2.py3-none-any.whl", hash = "sha256:cb7b6b79d33b15ae0a6e87856b984879927c7c2ebb29522df4c75b28ffd9b989", size = 29976, upload-time = "2023-12-24T20:45:17.154Z" }, +] + +[[package]] +name = "et-xmlfile" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "filelock" +version = "3.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, +] + +[[package]] +name = "flask" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/de/e47735752347f4128bcf354e0da07ef311a78244eba9e3dc1d4a5ab21a98/flask-3.1.1.tar.gz", hash = "sha256:284c7b8f2f58cb737f0cf1c30fd7eaf0ccfcde196099d24ecede3fc2005aa59e", size = 753440, upload-time = "2025-05-13T15:01:17.447Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/68/9d4508e893976286d2ead7f8f571314af6c2037af34853a30fd769c02e9d/flask-3.1.1-py3-none-any.whl", hash = "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c", size = 103305, upload-time = "2025-05-13T15:01:15.591Z" }, +] + +[[package]] +name = "flask-bootstrap" +version = "3.3.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dominate" }, + { name = "flask" }, + { name = "visitor" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/53/958ce7c2aa26280b7fd7f3eecbf13053f1302ee2acb1db58ef32e1c23c2a/Flask-Bootstrap-3.3.7.1.tar.gz", hash = "sha256:cb08ed940183f6343a64e465e83b3a3f13c53e1baabb8d72b5da4545ef123ac8", size = 456359, upload-time = "2017-01-11T23:28:23.944Z" } + +[[package]] +name = "flask-sqlalchemy" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/53/b0a9fcc1b1297f51e68b69ed3b7c3c40d8c45be1391d77ae198712914392/flask_sqlalchemy-3.1.1.tar.gz", hash = "sha256:e4b68bb881802dda1a7d878b2fc84c06d1ee57fb40b874d3dc97dabfa36b8312", size = 81899, upload-time = "2023-09-11T21:42:36.147Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/6a/89963a5c6ecf166e8be29e0d1bf6806051ee8fe6c82e232842e3aeac9204/flask_sqlalchemy-3.1.1-py3-none-any.whl", hash = "sha256:4ba4be7f419dc72f4efd8802d69974803c37259dd42f3913b0dcf75c9447e0a0", size = 25125, upload-time = "2023-09-11T21:42:34.514Z" }, +] + +[[package]] +name = "flask-webtest" +version = "0.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "flask" }, + { name = "webtest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/a0/7aa7d1097811d6cbd624fef10850307986c8ecd94aba58b6ca967e253562/flask_webtest-0.1.6.tar.gz", hash = "sha256:705b20e9b5c25a13e12564c1b40c94b96c209ed28eb468b43dad53e8a06f65ae", size = 7888, upload-time = "2024-08-22T22:52:22.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/08/0ced2ff1c6a1a62f27d56f6abcf14fc7cec7b17639ad1d4c0cd16a1e6c85/Flask_WebTest-0.1.6-py2.py3-none-any.whl", hash = "sha256:d9d508027dd8b44aff0bd53fa26207247136a500e8f7fd0c42319981627c9053", size = 6455, upload-time = "2024-08-22T22:52:20.992Z" }, +] + +[[package]] +name = "flask-wtf" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, + { name = "itsdangerous" }, + { name = "wtforms" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/9b/f1cd6e41bbf874f3436368f2c7ee3216c1e82d666ff90d1d800e20eb1317/flask_wtf-1.2.2.tar.gz", hash = "sha256:79d2ee1e436cf570bccb7d916533fa18757a2f18c290accffab1b9a0b684666b", size = 42641, upload-time = "2024-10-24T07:18:58.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/19/354449145fbebb65e7c621235b6ad69bebcfaec2142481f044d0ddc5b5c5/flask_wtf-1.2.2-py3-none-any.whl", hash = "sha256:e93160c5c5b6b571cf99300b6e01b72f9a101027cab1579901f8b10c5daf0b70", size = 12779, upload-time = "2024-10-24T07:18:56.976Z" }, +] + +[[package]] +name = "greenlet" +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/92/bb85bd6e80148a4d2e0c59f7c0c2891029f8fd510183afc7d8d2feeed9b6/greenlet-3.2.3.tar.gz", hash = "sha256:8b0dd8ae4c0d6f5e54ee55ba935eeb3d735a9b58a8a1e5b5cbab64e01a39f365", size = 185752, upload-time = "2025-06-05T16:16:09.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/db/b4c12cff13ebac2786f4f217f06588bccd8b53d260453404ef22b121fc3a/greenlet-3.2.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:1afd685acd5597349ee6d7a88a8bec83ce13c106ac78c196ee9dde7c04fe87be", size = 268977, upload-time = "2025-06-05T16:10:24.001Z" }, + { url = "https://files.pythonhosted.org/packages/52/61/75b4abd8147f13f70986df2801bf93735c1bd87ea780d70e3b3ecda8c165/greenlet-3.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:761917cac215c61e9dc7324b2606107b3b292a8349bdebb31503ab4de3f559ac", size = 627351, upload-time = "2025-06-05T16:38:50.685Z" }, + { url = "https://files.pythonhosted.org/packages/35/aa/6894ae299d059d26254779a5088632874b80ee8cf89a88bca00b0709d22f/greenlet-3.2.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a433dbc54e4a37e4fff90ef34f25a8c00aed99b06856f0119dcf09fbafa16392", size = 638599, upload-time = "2025-06-05T16:41:34.057Z" }, + { url = "https://files.pythonhosted.org/packages/30/64/e01a8261d13c47f3c082519a5e9dbf9e143cc0498ed20c911d04e54d526c/greenlet-3.2.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:72e77ed69312bab0434d7292316d5afd6896192ac4327d44f3d613ecb85b037c", size = 634482, upload-time = "2025-06-05T16:48:16.26Z" }, + { url = "https://files.pythonhosted.org/packages/47/48/ff9ca8ba9772d083a4f5221f7b4f0ebe8978131a9ae0909cf202f94cd879/greenlet-3.2.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:68671180e3849b963649254a882cd544a3c75bfcd2c527346ad8bb53494444db", size = 633284, upload-time = "2025-06-05T16:13:01.599Z" }, + { url = "https://files.pythonhosted.org/packages/e9/45/626e974948713bc15775b696adb3eb0bd708bec267d6d2d5c47bb47a6119/greenlet-3.2.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49c8cfb18fb419b3d08e011228ef8a25882397f3a859b9fe1436946140b6756b", size = 582206, upload-time = "2025-06-05T16:12:48.51Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8e/8b6f42c67d5df7db35b8c55c9a850ea045219741bb14416255616808c690/greenlet-3.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:efc6dc8a792243c31f2f5674b670b3a95d46fa1c6a912b8e310d6f542e7b0712", size = 1111412, upload-time = "2025-06-05T16:36:45.479Z" }, + { url = "https://files.pythonhosted.org/packages/05/46/ab58828217349500a7ebb81159d52ca357da747ff1797c29c6023d79d798/greenlet-3.2.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:731e154aba8e757aedd0781d4b240f1225b075b4409f1bb83b05ff410582cf00", size = 1135054, upload-time = "2025-06-05T16:12:36.478Z" }, + { url = "https://files.pythonhosted.org/packages/68/7f/d1b537be5080721c0f0089a8447d4ef72839039cdb743bdd8ffd23046e9a/greenlet-3.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:96c20252c2f792defe9a115d3287e14811036d51e78b3aaddbee23b69b216302", size = 296573, upload-time = "2025-06-05T16:34:26.521Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2e/d4fcb2978f826358b673f779f78fa8a32ee37df11920dc2bb5589cbeecef/greenlet-3.2.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:784ae58bba89fa1fa5733d170d42486580cab9decda3484779f4759345b29822", size = 270219, upload-time = "2025-06-05T16:10:10.414Z" }, + { url = "https://files.pythonhosted.org/packages/16/24/929f853e0202130e4fe163bc1d05a671ce8dcd604f790e14896adac43a52/greenlet-3.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0921ac4ea42a5315d3446120ad48f90c3a6b9bb93dd9b3cf4e4d84a66e42de83", size = 630383, upload-time = "2025-06-05T16:38:51.785Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b2/0320715eb61ae70c25ceca2f1d5ae620477d246692d9cc284c13242ec31c/greenlet-3.2.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d2971d93bb99e05f8c2c0c2f4aa9484a18d98c4c3bd3c62b65b7e6ae33dfcfaf", size = 642422, upload-time = "2025-06-05T16:41:35.259Z" }, + { url = "https://files.pythonhosted.org/packages/bd/49/445fd1a210f4747fedf77615d941444349c6a3a4a1135bba9701337cd966/greenlet-3.2.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c667c0bf9d406b77a15c924ef3285e1e05250948001220368e039b6aa5b5034b", size = 638375, upload-time = "2025-06-05T16:48:18.235Z" }, + { url = "https://files.pythonhosted.org/packages/7e/c8/ca19760cf6eae75fa8dc32b487e963d863b3ee04a7637da77b616703bc37/greenlet-3.2.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:592c12fb1165be74592f5de0d70f82bc5ba552ac44800d632214b76089945147", size = 637627, upload-time = "2025-06-05T16:13:02.858Z" }, + { url = "https://files.pythonhosted.org/packages/65/89/77acf9e3da38e9bcfca881e43b02ed467c1dedc387021fc4d9bd9928afb8/greenlet-3.2.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29e184536ba333003540790ba29829ac14bb645514fbd7e32af331e8202a62a5", size = 585502, upload-time = "2025-06-05T16:12:49.642Z" }, + { url = "https://files.pythonhosted.org/packages/97/c6/ae244d7c95b23b7130136e07a9cc5aadd60d59b5951180dc7dc7e8edaba7/greenlet-3.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:93c0bb79844a367782ec4f429d07589417052e621aa39a5ac1fb99c5aa308edc", size = 1114498, upload-time = "2025-06-05T16:36:46.598Z" }, + { url = "https://files.pythonhosted.org/packages/89/5f/b16dec0cbfd3070658e0d744487919740c6d45eb90946f6787689a7efbce/greenlet-3.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:751261fc5ad7b6705f5f76726567375bb2104a059454e0226e1eef6c756748ba", size = 1139977, upload-time = "2025-06-05T16:12:38.262Z" }, + { url = "https://files.pythonhosted.org/packages/66/77/d48fb441b5a71125bcac042fc5b1494c806ccb9a1432ecaa421e72157f77/greenlet-3.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:83a8761c75312361aa2b5b903b79da97f13f556164a7dd2d5448655425bd4c34", size = 297017, upload-time = "2025-06-05T16:25:05.225Z" }, + { url = "https://files.pythonhosted.org/packages/f3/94/ad0d435f7c48debe960c53b8f60fb41c2026b1d0fa4a99a1cb17c3461e09/greenlet-3.2.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:25ad29caed5783d4bd7a85c9251c651696164622494c00802a139c00d639242d", size = 271992, upload-time = "2025-06-05T16:11:23.467Z" }, + { url = "https://files.pythonhosted.org/packages/93/5d/7c27cf4d003d6e77749d299c7c8f5fd50b4f251647b5c2e97e1f20da0ab5/greenlet-3.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88cd97bf37fe24a6710ec6a3a7799f3f81d9cd33317dcf565ff9950c83f55e0b", size = 638820, upload-time = "2025-06-05T16:38:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7e/807e1e9be07a125bb4c169144937910bf59b9d2f6d931578e57f0bce0ae2/greenlet-3.2.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:baeedccca94880d2f5666b4fa16fc20ef50ba1ee353ee2d7092b383a243b0b0d", size = 653046, upload-time = "2025-06-05T16:41:36.343Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ab/158c1a4ea1068bdbc78dba5a3de57e4c7aeb4e7fa034320ea94c688bfb61/greenlet-3.2.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:be52af4b6292baecfa0f397f3edb3c6092ce071b499dd6fe292c9ac9f2c8f264", size = 647701, upload-time = "2025-06-05T16:48:19.604Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0d/93729068259b550d6a0288da4ff72b86ed05626eaf1eb7c0d3466a2571de/greenlet-3.2.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0cc73378150b8b78b0c9fe2ce56e166695e67478550769536a6742dca3651688", size = 649747, upload-time = "2025-06-05T16:13:04.628Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f6/c82ac1851c60851302d8581680573245c8fc300253fc1ff741ae74a6c24d/greenlet-3.2.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:706d016a03e78df129f68c4c9b4c4f963f7d73534e48a24f5f5a7101ed13dbbb", size = 605461, upload-time = "2025-06-05T16:12:50.792Z" }, + { url = "https://files.pythonhosted.org/packages/98/82/d022cf25ca39cf1200650fc58c52af32c90f80479c25d1cbf57980ec3065/greenlet-3.2.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:419e60f80709510c343c57b4bb5a339d8767bf9aef9b8ce43f4f143240f88b7c", size = 1121190, upload-time = "2025-06-05T16:36:48.59Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e1/25297f70717abe8104c20ecf7af0a5b82d2f5a980eb1ac79f65654799f9f/greenlet-3.2.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:93d48533fade144203816783373f27a97e4193177ebaaf0fc396db19e5d61163", size = 1149055, upload-time = "2025-06-05T16:12:40.457Z" }, + { url = "https://files.pythonhosted.org/packages/1f/8f/8f9e56c5e82eb2c26e8cde787962e66494312dc8cb261c460e1f3a9c88bc/greenlet-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:7454d37c740bb27bdeddfc3f358f26956a07d5220818ceb467a483197d84f849", size = 297817, upload-time = "2025-06-05T16:29:49.244Z" }, + { url = "https://files.pythonhosted.org/packages/b1/cf/f5c0b23309070ae93de75c90d29300751a5aacefc0a3ed1b1d8edb28f08b/greenlet-3.2.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:500b8689aa9dd1ab26872a34084503aeddefcb438e2e7317b89b11eaea1901ad", size = 270732, upload-time = "2025-06-05T16:10:08.26Z" }, + { url = "https://files.pythonhosted.org/packages/48/ae/91a957ba60482d3fecf9be49bc3948f341d706b52ddb9d83a70d42abd498/greenlet-3.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a07d3472c2a93117af3b0136f246b2833fdc0b542d4a9799ae5f41c28323faef", size = 639033, upload-time = "2025-06-05T16:38:53.983Z" }, + { url = "https://files.pythonhosted.org/packages/6f/df/20ffa66dd5a7a7beffa6451bdb7400d66251374ab40b99981478c69a67a8/greenlet-3.2.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:8704b3768d2f51150626962f4b9a9e4a17d2e37c8a8d9867bbd9fa4eb938d3b3", size = 652999, upload-time = "2025-06-05T16:41:37.89Z" }, + { url = "https://files.pythonhosted.org/packages/51/b4/ebb2c8cb41e521f1d72bf0465f2f9a2fd803f674a88db228887e6847077e/greenlet-3.2.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5035d77a27b7c62db6cf41cf786cfe2242644a7a337a0e155c80960598baab95", size = 647368, upload-time = "2025-06-05T16:48:21.467Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6a/1e1b5aa10dced4ae876a322155705257748108b7fd2e4fae3f2a091fe81a/greenlet-3.2.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2d8aa5423cd4a396792f6d4580f88bdc6efcb9205891c9d40d20f6e670992efb", size = 650037, upload-time = "2025-06-05T16:13:06.402Z" }, + { url = "https://files.pythonhosted.org/packages/26/f2/ad51331a157c7015c675702e2d5230c243695c788f8f75feba1af32b3617/greenlet-3.2.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c724620a101f8170065d7dded3f962a2aea7a7dae133a009cada42847e04a7b", size = 608402, upload-time = "2025-06-05T16:12:51.91Z" }, + { url = "https://files.pythonhosted.org/packages/26/bc/862bd2083e6b3aff23300900a956f4ea9a4059de337f5c8734346b9b34fc/greenlet-3.2.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:873abe55f134c48e1f2a6f53f7d1419192a3d1a4e873bace00499a4e45ea6af0", size = 1119577, upload-time = "2025-06-05T16:36:49.787Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/1fc0cc068cfde885170e01de40a619b00eaa8f2916bf3541744730ffb4c3/greenlet-3.2.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:024571bbce5f2c1cfff08bf3fbaa43bbc7444f580ae13b0099e95d0e6e67ed36", size = 1147121, upload-time = "2025-06-05T16:12:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/27/1a/199f9587e8cb08a0658f9c30f3799244307614148ffe8b1e3aa22f324dea/greenlet-3.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5195fb1e75e592dd04ce79881c8a22becdfa3e6f500e7feb059b1e6fdd54d3e3", size = 297603, upload-time = "2025-06-05T16:20:12.651Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ca/accd7aa5280eb92b70ed9e8f7fd79dc50a2c21d8c73b9a0856f5b564e222/greenlet-3.2.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:3d04332dddb10b4a211b68111dabaee2e1a073663d117dc10247b5b1642bac86", size = 271479, upload-time = "2025-06-05T16:10:47.525Z" }, + { url = "https://files.pythonhosted.org/packages/55/71/01ed9895d9eb49223280ecc98a557585edfa56b3d0e965b9fa9f7f06b6d9/greenlet-3.2.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8186162dffde068a465deab08fc72c767196895c39db26ab1c17c0b77a6d8b97", size = 683952, upload-time = "2025-06-05T16:38:55.125Z" }, + { url = "https://files.pythonhosted.org/packages/ea/61/638c4bdf460c3c678a0a1ef4c200f347dff80719597e53b5edb2fb27ab54/greenlet-3.2.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f4bfbaa6096b1b7a200024784217defedf46a07c2eee1a498e94a1b5f8ec5728", size = 696917, upload-time = "2025-06-05T16:41:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/22/cc/0bd1a7eb759d1f3e3cc2d1bc0f0b487ad3cc9f34d74da4b80f226fde4ec3/greenlet-3.2.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:ed6cfa9200484d234d8394c70f5492f144b20d4533f69262d530a1a082f6ee9a", size = 692443, upload-time = "2025-06-05T16:48:23.113Z" }, + { url = "https://files.pythonhosted.org/packages/67/10/b2a4b63d3f08362662e89c103f7fe28894a51ae0bc890fabf37d1d780e52/greenlet-3.2.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:02b0df6f63cd15012bed5401b47829cfd2e97052dc89da3cfaf2c779124eb892", size = 692995, upload-time = "2025-06-05T16:13:07.972Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c6/ad82f148a4e3ce9564056453a71529732baf5448ad53fc323e37efe34f66/greenlet-3.2.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86c2d68e87107c1792e2e8d5399acec2487a4e993ab76c792408e59394d52141", size = 655320, upload-time = "2025-06-05T16:12:53.453Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4f/aab73ecaa6b3086a4c89863d94cf26fa84cbff63f52ce9bc4342b3087a06/greenlet-3.2.3-cp314-cp314-win_amd64.whl", hash = "sha256:8c47aae8fbbfcf82cc13327ae802ba13c9c36753b67e760023fd116bc124a62a", size = 301236, upload-time = "2025-06-05T16:15:20.111Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hatch" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "hatchling" }, + { name = "httpx" }, + { name = "hyperlink" }, + { name = "keyring" }, + { name = "packaging" }, + { name = "pexpect" }, + { name = "platformdirs" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "tomli-w" }, + { name = "tomlkit" }, + { name = "userpath" }, + { name = "uv" }, + { name = "virtualenv" }, + { name = "zstandard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/43/c0b37db0e857a44ce5ffdb7e8a9b8fa6425d0b74dea698fafcd9bddb50d1/hatch-1.14.1.tar.gz", hash = "sha256:ca1aff788f8596b0dd1f8f8dfe776443d2724a86b1976fabaf087406ba3d0713", size = 5188180, upload-time = "2025-04-07T04:16:04.522Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/40/19c0935bf9f25808541a0e3144ac459de696c5b6b6d4511a98d456c69604/hatch-1.14.1-py3-none-any.whl", hash = "sha256:39cdaa59e47ce0c5505d88a951f4324a9c5aafa17e4a877e2fde79b36ab66c21", size = 125770, upload-time = "2025-04-07T04:16:02.525Z" }, +] + +[[package]] +name = "hatchling" +version = "1.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pathspec" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "trove-classifiers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/8a/cc1debe3514da292094f1c3a700e4ca25442489731ef7c0814358816bb03/hatchling-1.27.0.tar.gz", hash = "sha256:971c296d9819abb3811112fc52c7a9751c8d381898f36533bb16f9791e941fd6", size = 54983, upload-time = "2024-12-15T17:08:11.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/e7/ae38d7a6dfba0533684e0b2136817d667588ae3ec984c1a4e5df5eb88482/hatchling-1.27.0-py3-none-any.whl", hash = "sha256:d3a2f3567c4f926ea39849cdf924c7e99e6686c9c8e288ae1037c8fa2a5d937b", size = 75794, upload-time = "2024-12-15T17:08:10.364Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "hyperlink" +version = "21.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/51/1947bd81d75af87e3bb9e34593a4cf118115a8feb451ce7a69044ef1412e/hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", size = 140743, upload-time = "2021-01-08T05:51:20.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4", size = 74638, upload-time = "2021-01-08T05:51:22.906Z" }, +] + +[[package]] +name = "identify" +version = "2.6.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/88/d193a27416618628a5eea64e3223acd800b40749a96ffb322a9b55a49ed1/identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6", size = 99254, upload-time = "2025-05-23T20:37:53.3Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/cd/18f8da995b658420625f7ef13f037be53ae04ec5ad33f9b718240dcfd48c/identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2", size = 99145, upload-time = "2025-05-23T20:37:51.495Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912, upload-time = "2024-08-20T03:39:27.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825, upload-time = "2024-08-20T03:39:25.966Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/1c/831faaaa0f090b711c355c6d8b2abf277c72133aab472b6932b03322294c/jaraco_functools-4.2.1.tar.gz", hash = "sha256:be634abfccabce56fa3053f8c7ebe37b682683a4ee7793670ced17bab0087353", size = 19661, upload-time = "2025-06-21T19:22:03.201Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/fd/179a20f832824514df39a90bb0e5372b314fea99f217f5ab942b10a8a4e8/jaraco_functools-4.2.1-py3-none-any.whl", hash = "sha256:590486285803805f4b1f99c60ca9e94ed348d4added84b74c7a12885561e524e", size = 10349, upload-time = "2025-06-21T19:22:02.039Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + +[[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 = "keyring" +version = "25.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/09/d904a6e96f76ff214be59e7aa6ef7190008f52a0ab6689760a98de0bf37d/keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", size = 62750, upload-time = "2024-12-25T15:26:45.782Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085, upload-time = "2024-12-25T15:26:44.377Z" }, +] + +[[package]] +name = "legacy-cgi" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/ed/300cabc9693209d5a03e2ebc5eb5c4171b51607c08ed84a2b71c9015e0f3/legacy_cgi-2.6.3.tar.gz", hash = "sha256:4c119d6cb8e9d8b6ad7cc0ddad880552c62df4029622835d06dfd18f438a8154", size = 24401, upload-time = "2025-03-27T00:48:56.957Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/33/68c6c38193684537757e0d50a7ccb4f4656e5c2f7cd2be737a9d4a1bff71/legacy_cgi-2.6.3-py3-none-any.whl", hash = "sha256:6df2ea5ae14c71ef6f097f8b6372b44f6685283dc018535a75c924564183cdab", size = 19851, upload-time = "2025-03-27T00:48:55.366Z" }, +] + +[[package]] +name = "license-expression" +version = "30.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boolean-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/71/d89bb0e71b1415453980fd32315f2a037aad9f7f70f695c7cec7035feb13/license_expression-30.4.4.tar.gz", hash = "sha256:73448f0aacd8d0808895bdc4b2c8e01a8d67646e4188f887375398c761f340fd", size = 186402, upload-time = "2025-07-22T11:13:32.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/40/791891d4c0c4dab4c5e187c17261cedc26285fd41541577f900470a45a4d/license_expression-30.4.4-py3-none-any.whl", hash = "sha256:421788fdcadb41f049d2dc934ce666626265aeccefddd25e162a26f23bcbf8a4", size = 120615, upload-time = "2025-07-22T11:13:31.217Z" }, +] + +[[package]] +name = "lxml" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/ed/60eb6fa2923602fba988d9ca7c5cdbd7cf25faa795162ed538b527a35411/lxml-6.0.0.tar.gz", hash = "sha256:032e65120339d44cdc3efc326c9f660f5f7205f3a535c1fdbf898b29ea01fb72", size = 4096938, upload-time = "2025-06-26T16:28:19.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/e9/9c3ca02fbbb7585116c2e274b354a2d92b5c70561687dd733ec7b2018490/lxml-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:35bc626eec405f745199200ccb5c6b36f202675d204aa29bb52e27ba2b71dea8", size = 8399057, upload-time = "2025-06-26T16:25:02.169Z" }, + { url = "https://files.pythonhosted.org/packages/86/25/10a6e9001191854bf283515020f3633b1b1f96fd1b39aa30bf8fff7aa666/lxml-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:246b40f8a4aec341cbbf52617cad8ab7c888d944bfe12a6abd2b1f6cfb6f6082", size = 4569676, upload-time = "2025-06-26T16:25:05.431Z" }, + { url = "https://files.pythonhosted.org/packages/f5/a5/378033415ff61d9175c81de23e7ad20a3ffb614df4ffc2ffc86bc6746ffd/lxml-6.0.0-cp310-cp310-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:2793a627e95d119e9f1e19720730472f5543a6d84c50ea33313ce328d870f2dd", size = 5291361, upload-time = "2025-06-26T16:25:07.901Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a6/19c87c4f3b9362b08dc5452a3c3bce528130ac9105fc8fff97ce895ce62e/lxml-6.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:46b9ed911f36bfeb6338e0b482e7fe7c27d362c52fde29f221fddbc9ee2227e7", size = 5008290, upload-time = "2025-06-28T18:47:13.196Z" }, + { url = "https://files.pythonhosted.org/packages/09/d1/e9b7ad4b4164d359c4d87ed8c49cb69b443225cb495777e75be0478da5d5/lxml-6.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b4790b558bee331a933e08883c423f65bbcd07e278f91b2272489e31ab1e2b4", size = 5163192, upload-time = "2025-06-28T18:47:17.279Z" }, + { url = "https://files.pythonhosted.org/packages/56/d6/b3eba234dc1584744b0b374a7f6c26ceee5dc2147369a7e7526e25a72332/lxml-6.0.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2030956cf4886b10be9a0285c6802e078ec2391e1dd7ff3eb509c2c95a69b76", size = 5076973, upload-time = "2025-06-26T16:25:10.936Z" }, + { url = "https://files.pythonhosted.org/packages/8e/47/897142dd9385dcc1925acec0c4afe14cc16d310ce02c41fcd9010ac5d15d/lxml-6.0.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d23854ecf381ab1facc8f353dcd9adeddef3652268ee75297c1164c987c11dc", size = 5297795, upload-time = "2025-06-26T16:25:14.282Z" }, + { url = "https://files.pythonhosted.org/packages/fb/db/551ad84515c6f415cea70193a0ff11d70210174dc0563219f4ce711655c6/lxml-6.0.0-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:43fe5af2d590bf4691531b1d9a2495d7aab2090547eaacd224a3afec95706d76", size = 4776547, upload-time = "2025-06-26T16:25:17.123Z" }, + { url = "https://files.pythonhosted.org/packages/e0/14/c4a77ab4f89aaf35037a03c472f1ccc54147191888626079bd05babd6808/lxml-6.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74e748012f8c19b47f7d6321ac929a9a94ee92ef12bc4298c47e8b7219b26541", size = 5124904, upload-time = "2025-06-26T16:25:19.485Z" }, + { url = "https://files.pythonhosted.org/packages/70/b4/12ae6a51b8da106adec6a2e9c60f532350a24ce954622367f39269e509b1/lxml-6.0.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:43cfbb7db02b30ad3926e8fceaef260ba2fb7df787e38fa2df890c1ca7966c3b", size = 4805804, upload-time = "2025-06-26T16:25:21.949Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b6/2e82d34d49f6219cdcb6e3e03837ca5fb8b7f86c2f35106fb8610ac7f5b8/lxml-6.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34190a1ec4f1e84af256495436b2d196529c3f2094f0af80202947567fdbf2e7", size = 5323477, upload-time = "2025-06-26T16:25:24.475Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e6/b83ddc903b05cd08a5723fefd528eee84b0edd07bdf87f6c53a1fda841fd/lxml-6.0.0-cp310-cp310-win32.whl", hash = "sha256:5967fe415b1920a3877a4195e9a2b779249630ee49ece22021c690320ff07452", size = 3613840, upload-time = "2025-06-26T16:25:27.345Z" }, + { url = "https://files.pythonhosted.org/packages/40/af/874fb368dd0c663c030acb92612341005e52e281a102b72a4c96f42942e1/lxml-6.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:f3389924581d9a770c6caa4df4e74b606180869043b9073e2cec324bad6e306e", size = 3993584, upload-time = "2025-06-26T16:25:29.391Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f4/d296bc22c17d5607653008f6dd7b46afdfda12efd31021705b507df652bb/lxml-6.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:522fe7abb41309e9543b0d9b8b434f2b630c5fdaf6482bee642b34c8c70079c8", size = 3681400, upload-time = "2025-06-26T16:25:31.421Z" }, + { url = "https://files.pythonhosted.org/packages/7c/23/828d4cc7da96c611ec0ce6147bbcea2fdbde023dc995a165afa512399bbf/lxml-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ee56288d0df919e4aac43b539dd0e34bb55d6a12a6562038e8d6f3ed07f9e36", size = 8438217, upload-time = "2025-06-26T16:25:34.349Z" }, + { url = "https://files.pythonhosted.org/packages/f1/33/5ac521212c5bcb097d573145d54b2b4a3c9766cda88af5a0e91f66037c6e/lxml-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8dd6dd0e9c1992613ccda2bcb74fc9d49159dbe0f0ca4753f37527749885c25", size = 4590317, upload-time = "2025-06-26T16:25:38.103Z" }, + { url = "https://files.pythonhosted.org/packages/2b/2e/45b7ca8bee304c07f54933c37afe7dd4d39ff61ba2757f519dcc71bc5d44/lxml-6.0.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:d7ae472f74afcc47320238b5dbfd363aba111a525943c8a34a1b657c6be934c3", size = 5221628, upload-time = "2025-06-26T16:25:40.878Z" }, + { url = "https://files.pythonhosted.org/packages/32/23/526d19f7eb2b85da1f62cffb2556f647b049ebe2a5aa8d4d41b1fb2c7d36/lxml-6.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5592401cdf3dc682194727c1ddaa8aa0f3ddc57ca64fd03226a430b955eab6f6", size = 4949429, upload-time = "2025-06-28T18:47:20.046Z" }, + { url = "https://files.pythonhosted.org/packages/ac/cc/f6be27a5c656a43a5344e064d9ae004d4dcb1d3c9d4f323c8189ddfe4d13/lxml-6.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58ffd35bd5425c3c3b9692d078bf7ab851441434531a7e517c4984d5634cd65b", size = 5087909, upload-time = "2025-06-28T18:47:22.834Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e6/8ec91b5bfbe6972458bc105aeb42088e50e4b23777170404aab5dfb0c62d/lxml-6.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f720a14aa102a38907c6d5030e3d66b3b680c3e6f6bc95473931ea3c00c59967", size = 5031713, upload-time = "2025-06-26T16:25:43.226Z" }, + { url = "https://files.pythonhosted.org/packages/33/cf/05e78e613840a40e5be3e40d892c48ad3e475804db23d4bad751b8cadb9b/lxml-6.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2a5e8d207311a0170aca0eb6b160af91adc29ec121832e4ac151a57743a1e1e", size = 5232417, upload-time = "2025-06-26T16:25:46.111Z" }, + { url = "https://files.pythonhosted.org/packages/ac/8c/6b306b3e35c59d5f0b32e3b9b6b3b0739b32c0dc42a295415ba111e76495/lxml-6.0.0-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:2dd1cc3ea7e60bfb31ff32cafe07e24839df573a5e7c2d33304082a5019bcd58", size = 4681443, upload-time = "2025-06-26T16:25:48.837Z" }, + { url = "https://files.pythonhosted.org/packages/59/43/0bd96bece5f7eea14b7220476835a60d2b27f8e9ca99c175f37c085cb154/lxml-6.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2cfcf84f1defed7e5798ef4f88aa25fcc52d279be731ce904789aa7ccfb7e8d2", size = 5074542, upload-time = "2025-06-26T16:25:51.65Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3d/32103036287a8ca012d8518071f8852c68f2b3bfe048cef2a0202eb05910/lxml-6.0.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:a52a4704811e2623b0324a18d41ad4b9fabf43ce5ff99b14e40a520e2190c851", size = 4729471, upload-time = "2025-06-26T16:25:54.571Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a8/7be5d17df12d637d81854bd8648cd329f29640a61e9a72a3f77add4a311b/lxml-6.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c16304bba98f48a28ae10e32a8e75c349dd742c45156f297e16eeb1ba9287a1f", size = 5256285, upload-time = "2025-06-26T16:25:56.997Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d0/6cb96174c25e0d749932557c8d51d60c6e292c877b46fae616afa23ed31a/lxml-6.0.0-cp311-cp311-win32.whl", hash = "sha256:f8d19565ae3eb956d84da3ef367aa7def14a2735d05bd275cd54c0301f0d0d6c", size = 3612004, upload-time = "2025-06-26T16:25:59.11Z" }, + { url = "https://files.pythonhosted.org/packages/ca/77/6ad43b165dfc6dead001410adeb45e88597b25185f4479b7ca3b16a5808f/lxml-6.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b2d71cdefda9424adff9a3607ba5bbfc60ee972d73c21c7e3c19e71037574816", size = 4003470, upload-time = "2025-06-26T16:26:01.655Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bc/4c50ec0eb14f932a18efc34fc86ee936a66c0eb5f2fe065744a2da8a68b2/lxml-6.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:8a2e76efbf8772add72d002d67a4c3d0958638696f541734304c7f28217a9cab", size = 3682477, upload-time = "2025-06-26T16:26:03.808Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/d01d735c298d7e0ddcedf6f028bf556577e5ab4f4da45175ecd909c79378/lxml-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78718d8454a6e928470d511bf8ac93f469283a45c354995f7d19e77292f26108", size = 8429515, upload-time = "2025-06-26T16:26:06.776Z" }, + { url = "https://files.pythonhosted.org/packages/06/37/0e3eae3043d366b73da55a86274a590bae76dc45aa004b7042e6f97803b1/lxml-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:84ef591495ffd3f9dcabffd6391db7bb70d7230b5c35ef5148354a134f56f2be", size = 4601387, upload-time = "2025-06-26T16:26:09.511Z" }, + { url = "https://files.pythonhosted.org/packages/a3/28/e1a9a881e6d6e29dda13d633885d13acb0058f65e95da67841c8dd02b4a8/lxml-6.0.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:2930aa001a3776c3e2601cb8e0a15d21b8270528d89cc308be4843ade546b9ab", size = 5228928, upload-time = "2025-06-26T16:26:12.337Z" }, + { url = "https://files.pythonhosted.org/packages/9a/55/2cb24ea48aa30c99f805921c1c7860c1f45c0e811e44ee4e6a155668de06/lxml-6.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:219e0431ea8006e15005767f0351e3f7f9143e793e58519dc97fe9e07fae5563", size = 4952289, upload-time = "2025-06-28T18:47:25.602Z" }, + { url = "https://files.pythonhosted.org/packages/31/c0/b25d9528df296b9a3306ba21ff982fc5b698c45ab78b94d18c2d6ae71fd9/lxml-6.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bd5913b4972681ffc9718bc2d4c53cde39ef81415e1671ff93e9aa30b46595e7", size = 5111310, upload-time = "2025-06-28T18:47:28.136Z" }, + { url = "https://files.pythonhosted.org/packages/e9/af/681a8b3e4f668bea6e6514cbcb297beb6de2b641e70f09d3d78655f4f44c/lxml-6.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:390240baeb9f415a82eefc2e13285016f9c8b5ad71ec80574ae8fa9605093cd7", size = 5025457, upload-time = "2025-06-26T16:26:15.068Z" }, + { url = "https://files.pythonhosted.org/packages/99/b6/3a7971aa05b7be7dfebc7ab57262ec527775c2c3c5b2f43675cac0458cad/lxml-6.0.0-cp312-cp312-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d6e200909a119626744dd81bae409fc44134389e03fbf1d68ed2a55a2fb10991", size = 5657016, upload-time = "2025-07-03T19:19:06.008Z" }, + { url = "https://files.pythonhosted.org/packages/69/f8/693b1a10a891197143c0673fcce5b75fc69132afa81a36e4568c12c8faba/lxml-6.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca50bd612438258a91b5b3788c6621c1f05c8c478e7951899f492be42defc0da", size = 5257565, upload-time = "2025-06-26T16:26:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/96/e08ff98f2c6426c98c8964513c5dab8d6eb81dadcd0af6f0c538ada78d33/lxml-6.0.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:c24b8efd9c0f62bad0439283c2c795ef916c5a6b75f03c17799775c7ae3c0c9e", size = 4713390, upload-time = "2025-06-26T16:26:20.292Z" }, + { url = "https://files.pythonhosted.org/packages/a8/83/6184aba6cc94d7413959f6f8f54807dc318fdcd4985c347fe3ea6937f772/lxml-6.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:afd27d8629ae94c5d863e32ab0e1d5590371d296b87dae0a751fb22bf3685741", size = 5066103, upload-time = "2025-06-26T16:26:22.765Z" }, + { url = "https://files.pythonhosted.org/packages/ee/01/8bf1f4035852d0ff2e36a4d9aacdbcc57e93a6cd35a54e05fa984cdf73ab/lxml-6.0.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:54c4855eabd9fc29707d30141be99e5cd1102e7d2258d2892314cf4c110726c3", size = 4791428, upload-time = "2025-06-26T16:26:26.461Z" }, + { url = "https://files.pythonhosted.org/packages/29/31/c0267d03b16954a85ed6b065116b621d37f559553d9339c7dcc4943a76f1/lxml-6.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c907516d49f77f6cd8ead1322198bdfd902003c3c330c77a1c5f3cc32a0e4d16", size = 5678523, upload-time = "2025-07-03T19:19:09.837Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f7/5495829a864bc5f8b0798d2b52a807c89966523140f3d6fa3a58ab6720ea/lxml-6.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36531f81c8214e293097cd2b7873f178997dae33d3667caaae8bdfb9666b76c0", size = 5281290, upload-time = "2025-06-26T16:26:29.406Z" }, + { url = "https://files.pythonhosted.org/packages/79/56/6b8edb79d9ed294ccc4e881f4db1023af56ba451909b9ce79f2a2cd7c532/lxml-6.0.0-cp312-cp312-win32.whl", hash = "sha256:690b20e3388a7ec98e899fd54c924e50ba6693874aa65ef9cb53de7f7de9d64a", size = 3613495, upload-time = "2025-06-26T16:26:31.588Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1e/cc32034b40ad6af80b6fd9b66301fc0f180f300002e5c3eb5a6110a93317/lxml-6.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:310b719b695b3dd442cdfbbe64936b2f2e231bb91d998e99e6f0daf991a3eba3", size = 4014711, upload-time = "2025-06-26T16:26:33.723Z" }, + { url = "https://files.pythonhosted.org/packages/55/10/dc8e5290ae4c94bdc1a4c55865be7e1f31dfd857a88b21cbba68b5fea61b/lxml-6.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:8cb26f51c82d77483cdcd2b4a53cda55bbee29b3c2f3ddeb47182a2a9064e4eb", size = 3674431, upload-time = "2025-06-26T16:26:35.959Z" }, + { url = "https://files.pythonhosted.org/packages/79/21/6e7c060822a3c954ff085e5e1b94b4a25757c06529eac91e550f3f5cd8b8/lxml-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6da7cd4f405fd7db56e51e96bff0865b9853ae70df0e6720624049da76bde2da", size = 8414372, upload-time = "2025-06-26T16:26:39.079Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f6/051b1607a459db670fc3a244fa4f06f101a8adf86cda263d1a56b3a4f9d5/lxml-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b34339898bb556a2351a1830f88f751679f343eabf9cf05841c95b165152c9e7", size = 4593940, upload-time = "2025-06-26T16:26:41.891Z" }, + { url = "https://files.pythonhosted.org/packages/8e/74/dd595d92a40bda3c687d70d4487b2c7eff93fd63b568acd64fedd2ba00fe/lxml-6.0.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:51a5e4c61a4541bd1cd3ba74766d0c9b6c12d6a1a4964ef60026832aac8e79b3", size = 5214329, upload-time = "2025-06-26T16:26:44.669Z" }, + { url = "https://files.pythonhosted.org/packages/52/46/3572761efc1bd45fcafb44a63b3b0feeb5b3f0066886821e94b0254f9253/lxml-6.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d18a25b19ca7307045581b18b3ec9ead2b1db5ccd8719c291f0cd0a5cec6cb81", size = 4947559, upload-time = "2025-06-28T18:47:31.091Z" }, + { url = "https://files.pythonhosted.org/packages/94/8a/5e40de920e67c4f2eef9151097deb9b52d86c95762d8ee238134aff2125d/lxml-6.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d4f0c66df4386b75d2ab1e20a489f30dc7fd9a06a896d64980541506086be1f1", size = 5102143, upload-time = "2025-06-28T18:47:33.612Z" }, + { url = "https://files.pythonhosted.org/packages/7c/4b/20555bdd75d57945bdabfbc45fdb1a36a1a0ff9eae4653e951b2b79c9209/lxml-6.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f4b481b6cc3a897adb4279216695150bbe7a44c03daba3c894f49d2037e0a24", size = 5021931, upload-time = "2025-06-26T16:26:47.503Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/cf03b412f3763d4ca23b25e70c96a74cfece64cec3addf1c4ec639586b13/lxml-6.0.0-cp313-cp313-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a78d6c9168f5bcb20971bf3329c2b83078611fbe1f807baadc64afc70523b3a", size = 5645469, upload-time = "2025-07-03T19:19:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dd/39c8507c16db6031f8c1ddf70ed95dbb0a6d466a40002a3522c128aba472/lxml-6.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae06fbab4f1bb7db4f7c8ca9897dc8db4447d1a2b9bee78474ad403437bcc29", size = 5247467, upload-time = "2025-06-26T16:26:49.998Z" }, + { url = "https://files.pythonhosted.org/packages/4d/56/732d49def0631ad633844cfb2664563c830173a98d5efd9b172e89a4800d/lxml-6.0.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:1fa377b827ca2023244a06554c6e7dc6828a10aaf74ca41965c5d8a4925aebb4", size = 4720601, upload-time = "2025-06-26T16:26:52.564Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7f/6b956fab95fa73462bca25d1ea7fc8274ddf68fb8e60b78d56c03b65278e/lxml-6.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1676b56d48048a62ef77a250428d1f31f610763636e0784ba67a9740823988ca", size = 5060227, upload-time = "2025-06-26T16:26:55.054Z" }, + { url = "https://files.pythonhosted.org/packages/97/06/e851ac2924447e8b15a294855caf3d543424364a143c001014d22c8ca94c/lxml-6.0.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:0e32698462aacc5c1cf6bdfebc9c781821b7e74c79f13e5ffc8bfe27c42b1abf", size = 4790637, upload-time = "2025-06-26T16:26:57.384Z" }, + { url = "https://files.pythonhosted.org/packages/06/d4/fd216f3cd6625022c25b336c7570d11f4a43adbaf0a56106d3d496f727a7/lxml-6.0.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4d6036c3a296707357efb375cfc24bb64cd955b9ec731abf11ebb1e40063949f", size = 5662049, upload-time = "2025-07-03T19:19:16.409Z" }, + { url = "https://files.pythonhosted.org/packages/52/03/0e764ce00b95e008d76b99d432f1807f3574fb2945b496a17807a1645dbd/lxml-6.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7488a43033c958637b1a08cddc9188eb06d3ad36582cebc7d4815980b47e27ef", size = 5272430, upload-time = "2025-06-26T16:27:00.031Z" }, + { url = "https://files.pythonhosted.org/packages/5f/01/d48cc141bc47bc1644d20fe97bbd5e8afb30415ec94f146f2f76d0d9d098/lxml-6.0.0-cp313-cp313-win32.whl", hash = "sha256:5fcd7d3b1d8ecb91445bd71b9c88bdbeae528fefee4f379895becfc72298d181", size = 3612896, upload-time = "2025-06-26T16:27:04.251Z" }, + { url = "https://files.pythonhosted.org/packages/f4/87/6456b9541d186ee7d4cb53bf1b9a0d7f3b1068532676940fdd594ac90865/lxml-6.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:2f34687222b78fff795feeb799a7d44eca2477c3d9d3a46ce17d51a4f383e32e", size = 4013132, upload-time = "2025-06-26T16:27:06.415Z" }, + { url = "https://files.pythonhosted.org/packages/b7/42/85b3aa8f06ca0d24962f8100f001828e1f1f1a38c954c16e71154ed7d53a/lxml-6.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:21db1ec5525780fd07251636eb5f7acb84003e9382c72c18c542a87c416ade03", size = 3672642, upload-time = "2025-06-26T16:27:09.888Z" }, + { url = "https://files.pythonhosted.org/packages/66/e1/2c22a3cff9e16e1d717014a1e6ec2bf671bf56ea8716bb64466fcf820247/lxml-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:dbdd7679a6f4f08152818043dbb39491d1af3332128b3752c3ec5cebc0011a72", size = 3898804, upload-time = "2025-06-26T16:27:59.751Z" }, + { url = "https://files.pythonhosted.org/packages/2b/3a/d68cbcb4393a2a0a867528741fafb7ce92dac5c9f4a1680df98e5e53e8f5/lxml-6.0.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40442e2a4456e9910875ac12951476d36c0870dcb38a68719f8c4686609897c4", size = 4216406, upload-time = "2025-06-28T18:47:45.518Z" }, + { url = "https://files.pythonhosted.org/packages/15/8f/d9bfb13dff715ee3b2a1ec2f4a021347ea3caf9aba93dea0cfe54c01969b/lxml-6.0.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db0efd6bae1c4730b9c863fc4f5f3c0fa3e8f05cae2c44ae141cb9dfc7d091dc", size = 4326455, upload-time = "2025-06-28T18:47:48.411Z" }, + { url = "https://files.pythonhosted.org/packages/01/8b/fde194529ee8a27e6f5966d7eef05fa16f0567e4a8e8abc3b855ef6b3400/lxml-6.0.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ab542c91f5a47aaa58abdd8ea84b498e8e49fe4b883d67800017757a3eb78e8", size = 4268788, upload-time = "2025-06-26T16:28:02.776Z" }, + { url = "https://files.pythonhosted.org/packages/99/a8/3b8e2581b4f8370fc9e8dc343af4abdfadd9b9229970fc71e67bd31c7df1/lxml-6.0.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:013090383863b72c62a702d07678b658fa2567aa58d373d963cca245b017e065", size = 4411394, upload-time = "2025-06-26T16:28:05.179Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a5/899a4719e02ff4383f3f96e5d1878f882f734377f10dfb69e73b5f223e44/lxml-6.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c86df1c9af35d903d2b52d22ea3e66db8058d21dc0f59842ca5deb0595921141", size = 3517946, upload-time = "2025-06-26T16:28:07.665Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "more-itertools" +version = "10.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/a0/834b0cebabbfc7e311f30b46c8188790a37f89fc8d756660346fe5abfd09/more_itertools-10.7.0.tar.gz", hash = "sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3", size = 127671, upload-time = "2025-04-22T14:17:41.838Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/9f/7ba6f94fc1e9ac3d2b853fdff3035fb2fa5afbed898c4a72b8a020610594/more_itertools-10.7.0-py3-none-any.whl", hash = "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e", size = 65278, upload-time = "2025-04-22T14:17:40.49Z" }, +] + +[[package]] +name = "morphi" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "pytz" }, + { name = "speaklater" }, + { name = "tomli" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/ea/03783147fa3321b4e7d1aa2950c578f0cb4188b985af21e0a16fb37db37f/morphi-0.3.2.tar.gz", hash = "sha256:bda9d1f12045d694ddf204b1fc44e43caf0ab792a599b818be8188e3b781c08a", size = 18438, upload-time = "2025-07-10T12:49:30.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/70/1adea3a3ea81fe5f6ddbd4982bd658194b983b0087abc977aace03c7b5ca/morphi-0.3.2-py2.py3-none-any.whl", hash = "sha256:281fe08463eda365fe257c859b2c892eb6b7ecec77255a16cff002ae21ee0b08", size = 17841, upload-time = "2025-07-10T12:49:29.048Z" }, +] + +[[package]] +name = "msgpack" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/b1/ea4f68038a18c77c9467400d166d74c4ffa536f34761f7983a104357e614/msgpack-1.1.1.tar.gz", hash = "sha256:77b79ce34a2bdab2594f490c8e80dd62a02d650b91a75159a63ec413b8d104cd", size = 173555, upload-time = "2025-06-13T06:52:51.324Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/52/f30da112c1dc92cf64f57d08a273ac771e7b29dea10b4b30369b2d7e8546/msgpack-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:353b6fc0c36fde68b661a12949d7d49f8f51ff5fa019c1e47c87c4ff34b080ed", size = 81799, upload-time = "2025-06-13T06:51:37.228Z" }, + { url = "https://files.pythonhosted.org/packages/e4/35/7bfc0def2f04ab4145f7f108e3563f9b4abae4ab0ed78a61f350518cc4d2/msgpack-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:79c408fcf76a958491b4e3b103d1c417044544b68e96d06432a189b43d1215c8", size = 78278, upload-time = "2025-06-13T06:51:38.534Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c5/df5d6c1c39856bc55f800bf82778fd4c11370667f9b9e9d51b2f5da88f20/msgpack-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78426096939c2c7482bf31ef15ca219a9e24460289c00dd0b94411040bb73ad2", size = 402805, upload-time = "2025-06-13T06:51:39.538Z" }, + { url = "https://files.pythonhosted.org/packages/20/8e/0bb8c977efecfe6ea7116e2ed73a78a8d32a947f94d272586cf02a9757db/msgpack-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b17ba27727a36cb73aabacaa44b13090feb88a01d012c0f4be70c00f75048b4", size = 408642, upload-time = "2025-06-13T06:51:41.092Z" }, + { url = "https://files.pythonhosted.org/packages/59/a1/731d52c1aeec52006be6d1f8027c49fdc2cfc3ab7cbe7c28335b2910d7b6/msgpack-1.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a17ac1ea6ec3c7687d70201cfda3b1e8061466f28f686c24f627cae4ea8efd0", size = 395143, upload-time = "2025-06-13T06:51:42.575Z" }, + { url = "https://files.pythonhosted.org/packages/2b/92/b42911c52cda2ba67a6418ffa7d08969edf2e760b09015593c8a8a27a97d/msgpack-1.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:88d1e966c9235c1d4e2afac21ca83933ba59537e2e2727a999bf3f515ca2af26", size = 395986, upload-time = "2025-06-13T06:51:43.807Z" }, + { url = "https://files.pythonhosted.org/packages/61/dc/8ae165337e70118d4dab651b8b562dd5066dd1e6dd57b038f32ebc3e2f07/msgpack-1.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f6d58656842e1b2ddbe07f43f56b10a60f2ba5826164910968f5933e5178af75", size = 402682, upload-time = "2025-06-13T06:51:45.534Z" }, + { url = "https://files.pythonhosted.org/packages/58/27/555851cb98dcbd6ce041df1eacb25ac30646575e9cd125681aa2f4b1b6f1/msgpack-1.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:96decdfc4adcbc087f5ea7ebdcfd3dee9a13358cae6e81d54be962efc38f6338", size = 406368, upload-time = "2025-06-13T06:51:46.97Z" }, + { url = "https://files.pythonhosted.org/packages/d4/64/39a26add4ce16f24e99eabb9005e44c663db00e3fce17d4ae1ae9d61df99/msgpack-1.1.1-cp310-cp310-win32.whl", hash = "sha256:6640fd979ca9a212e4bcdf6eb74051ade2c690b862b679bfcb60ae46e6dc4bfd", size = 65004, upload-time = "2025-06-13T06:51:48.582Z" }, + { url = "https://files.pythonhosted.org/packages/7d/18/73dfa3e9d5d7450d39debde5b0d848139f7de23bd637a4506e36c9800fd6/msgpack-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:8b65b53204fe1bd037c40c4148d00ef918eb2108d24c9aaa20bc31f9810ce0a8", size = 71548, upload-time = "2025-06-13T06:51:49.558Z" }, + { url = "https://files.pythonhosted.org/packages/7f/83/97f24bf9848af23fe2ba04380388216defc49a8af6da0c28cc636d722502/msgpack-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:71ef05c1726884e44f8b1d1773604ab5d4d17729d8491403a705e649116c9558", size = 82728, upload-time = "2025-06-13T06:51:50.68Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7f/2eaa388267a78401f6e182662b08a588ef4f3de6f0eab1ec09736a7aaa2b/msgpack-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:36043272c6aede309d29d56851f8841ba907a1a3d04435e43e8a19928e243c1d", size = 79279, upload-time = "2025-06-13T06:51:51.72Z" }, + { url = "https://files.pythonhosted.org/packages/f8/46/31eb60f4452c96161e4dfd26dbca562b4ec68c72e4ad07d9566d7ea35e8a/msgpack-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a32747b1b39c3ac27d0670122b57e6e57f28eefb725e0b625618d1b59bf9d1e0", size = 423859, upload-time = "2025-06-13T06:51:52.749Z" }, + { url = "https://files.pythonhosted.org/packages/45/16/a20fa8c32825cc7ae8457fab45670c7a8996d7746ce80ce41cc51e3b2bd7/msgpack-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a8b10fdb84a43e50d38057b06901ec9da52baac6983d3f709d8507f3889d43f", size = 429975, upload-time = "2025-06-13T06:51:53.97Z" }, + { url = "https://files.pythonhosted.org/packages/86/ea/6c958e07692367feeb1a1594d35e22b62f7f476f3c568b002a5ea09d443d/msgpack-1.1.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba0c325c3f485dc54ec298d8b024e134acf07c10d494ffa24373bea729acf704", size = 413528, upload-time = "2025-06-13T06:51:55.507Z" }, + { url = "https://files.pythonhosted.org/packages/75/05/ac84063c5dae79722bda9f68b878dc31fc3059adb8633c79f1e82c2cd946/msgpack-1.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:88daaf7d146e48ec71212ce21109b66e06a98e5e44dca47d853cbfe171d6c8d2", size = 413338, upload-time = "2025-06-13T06:51:57.023Z" }, + { url = "https://files.pythonhosted.org/packages/69/e8/fe86b082c781d3e1c09ca0f4dacd457ede60a13119b6ce939efe2ea77b76/msgpack-1.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8b55ea20dc59b181d3f47103f113e6f28a5e1c89fd5b67b9140edb442ab67f2", size = 422658, upload-time = "2025-06-13T06:51:58.419Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2b/bafc9924df52d8f3bb7c00d24e57be477f4d0f967c0a31ef5e2225e035c7/msgpack-1.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4a28e8072ae9779f20427af07f53bbb8b4aa81151054e882aee333b158da8752", size = 427124, upload-time = "2025-06-13T06:51:59.969Z" }, + { url = "https://files.pythonhosted.org/packages/a2/3b/1f717e17e53e0ed0b68fa59e9188f3f610c79d7151f0e52ff3cd8eb6b2dc/msgpack-1.1.1-cp311-cp311-win32.whl", hash = "sha256:7da8831f9a0fdb526621ba09a281fadc58ea12701bc709e7b8cbc362feabc295", size = 65016, upload-time = "2025-06-13T06:52:01.294Z" }, + { url = "https://files.pythonhosted.org/packages/48/45/9d1780768d3b249accecc5a38c725eb1e203d44a191f7b7ff1941f7df60c/msgpack-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:5fd1b58e1431008a57247d6e7cc4faa41c3607e8e7d4aaf81f7c29ea013cb458", size = 72267, upload-time = "2025-06-13T06:52:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/e3/26/389b9c593eda2b8551b2e7126ad3a06af6f9b44274eb3a4f054d48ff7e47/msgpack-1.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ae497b11f4c21558d95de9f64fff7053544f4d1a17731c866143ed6bb4591238", size = 82359, upload-time = "2025-06-13T06:52:03.909Z" }, + { url = "https://files.pythonhosted.org/packages/ab/65/7d1de38c8a22cf8b1551469159d4b6cf49be2126adc2482de50976084d78/msgpack-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:33be9ab121df9b6b461ff91baac6f2731f83d9b27ed948c5b9d1978ae28bf157", size = 79172, upload-time = "2025-06-13T06:52:05.246Z" }, + { url = "https://files.pythonhosted.org/packages/0f/bd/cacf208b64d9577a62c74b677e1ada005caa9b69a05a599889d6fc2ab20a/msgpack-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f64ae8fe7ffba251fecb8408540c34ee9df1c26674c50c4544d72dbf792e5ce", size = 425013, upload-time = "2025-06-13T06:52:06.341Z" }, + { url = "https://files.pythonhosted.org/packages/4d/ec/fd869e2567cc9c01278a736cfd1697941ba0d4b81a43e0aa2e8d71dab208/msgpack-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a494554874691720ba5891c9b0b39474ba43ffb1aaf32a5dac874effb1619e1a", size = 426905, upload-time = "2025-06-13T06:52:07.501Z" }, + { url = "https://files.pythonhosted.org/packages/55/2a/35860f33229075bce803a5593d046d8b489d7ba2fc85701e714fc1aaf898/msgpack-1.1.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb643284ab0ed26f6957d969fe0dd8bb17beb567beb8998140b5e38a90974f6c", size = 407336, upload-time = "2025-06-13T06:52:09.047Z" }, + { url = "https://files.pythonhosted.org/packages/8c/16/69ed8f3ada150bf92745fb4921bd621fd2cdf5a42e25eb50bcc57a5328f0/msgpack-1.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d275a9e3c81b1093c060c3837e580c37f47c51eca031f7b5fb76f7b8470f5f9b", size = 409485, upload-time = "2025-06-13T06:52:10.382Z" }, + { url = "https://files.pythonhosted.org/packages/c6/b6/0c398039e4c6d0b2e37c61d7e0e9d13439f91f780686deb8ee64ecf1ae71/msgpack-1.1.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fd6b577e4541676e0cc9ddc1709d25014d3ad9a66caa19962c4f5de30fc09ef", size = 412182, upload-time = "2025-06-13T06:52:11.644Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d0/0cf4a6ecb9bc960d624c93effaeaae75cbf00b3bc4a54f35c8507273cda1/msgpack-1.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb29aaa613c0a1c40d1af111abf025f1732cab333f96f285d6a93b934738a68a", size = 419883, upload-time = "2025-06-13T06:52:12.806Z" }, + { url = "https://files.pythonhosted.org/packages/62/83/9697c211720fa71a2dfb632cad6196a8af3abea56eece220fde4674dc44b/msgpack-1.1.1-cp312-cp312-win32.whl", hash = "sha256:870b9a626280c86cff9c576ec0d9cbcc54a1e5ebda9cd26dab12baf41fee218c", size = 65406, upload-time = "2025-06-13T06:52:14.271Z" }, + { url = "https://files.pythonhosted.org/packages/c0/23/0abb886e80eab08f5e8c485d6f13924028602829f63b8f5fa25a06636628/msgpack-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:5692095123007180dca3e788bb4c399cc26626da51629a31d40207cb262e67f4", size = 72558, upload-time = "2025-06-13T06:52:15.252Z" }, + { url = "https://files.pythonhosted.org/packages/a1/38/561f01cf3577430b59b340b51329803d3a5bf6a45864a55f4ef308ac11e3/msgpack-1.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3765afa6bd4832fc11c3749be4ba4b69a0e8d7b728f78e68120a157a4c5d41f0", size = 81677, upload-time = "2025-06-13T06:52:16.64Z" }, + { url = "https://files.pythonhosted.org/packages/09/48/54a89579ea36b6ae0ee001cba8c61f776451fad3c9306cd80f5b5c55be87/msgpack-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8ddb2bcfd1a8b9e431c8d6f4f7db0773084e107730ecf3472f1dfe9ad583f3d9", size = 78603, upload-time = "2025-06-13T06:52:17.843Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/daba2699b308e95ae792cdc2ef092a38eb5ee422f9d2fbd4101526d8a210/msgpack-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:196a736f0526a03653d829d7d4c5500a97eea3648aebfd4b6743875f28aa2af8", size = 420504, upload-time = "2025-06-13T06:52:18.982Z" }, + { url = "https://files.pythonhosted.org/packages/20/22/2ebae7ae43cd8f2debc35c631172ddf14e2a87ffcc04cf43ff9df9fff0d3/msgpack-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d592d06e3cc2f537ceeeb23d38799c6ad83255289bb84c2e5792e5a8dea268a", size = 423749, upload-time = "2025-06-13T06:52:20.211Z" }, + { url = "https://files.pythonhosted.org/packages/40/1b/54c08dd5452427e1179a40b4b607e37e2664bca1c790c60c442c8e972e47/msgpack-1.1.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4df2311b0ce24f06ba253fda361f938dfecd7b961576f9be3f3fbd60e87130ac", size = 404458, upload-time = "2025-06-13T06:52:21.429Z" }, + { url = "https://files.pythonhosted.org/packages/2e/60/6bb17e9ffb080616a51f09928fdd5cac1353c9becc6c4a8abd4e57269a16/msgpack-1.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e4141c5a32b5e37905b5940aacbc59739f036930367d7acce7a64e4dec1f5e0b", size = 405976, upload-time = "2025-06-13T06:52:22.995Z" }, + { url = "https://files.pythonhosted.org/packages/ee/97/88983e266572e8707c1f4b99c8fd04f9eb97b43f2db40e3172d87d8642db/msgpack-1.1.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b1ce7f41670c5a69e1389420436f41385b1aa2504c3b0c30620764b15dded2e7", size = 408607, upload-time = "2025-06-13T06:52:24.152Z" }, + { url = "https://files.pythonhosted.org/packages/bc/66/36c78af2efaffcc15a5a61ae0df53a1d025f2680122e2a9eb8442fed3ae4/msgpack-1.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4147151acabb9caed4e474c3344181e91ff7a388b888f1e19ea04f7e73dc7ad5", size = 424172, upload-time = "2025-06-13T06:52:25.704Z" }, + { url = "https://files.pythonhosted.org/packages/8c/87/a75eb622b555708fe0427fab96056d39d4c9892b0c784b3a721088c7ee37/msgpack-1.1.1-cp313-cp313-win32.whl", hash = "sha256:500e85823a27d6d9bba1d057c871b4210c1dd6fb01fbb764e37e4e8847376323", size = 65347, upload-time = "2025-06-13T06:52:26.846Z" }, + { url = "https://files.pythonhosted.org/packages/ca/91/7dc28d5e2a11a5ad804cf2b7f7a5fcb1eb5a4966d66a5d2b41aee6376543/msgpack-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:6d489fba546295983abd142812bda76b57e33d0b9f5d5b71c09a583285506f69", size = 72341, upload-time = "2025-06-13T06:52:27.835Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, +] + +[[package]] +name = "nox" +version = "2025.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argcomplete" }, + { name = "attrs" }, + { name = "colorlog" }, + { name = "dependency-groups" }, + { name = "packaging" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/80/47712208c410defec169992e57c179f0f4d92f5dd17ba8daca50a8077e23/nox-2025.5.1.tar.gz", hash = "sha256:2a571dfa7a58acc726521ac3cd8184455ebcdcbf26401c7b737b5bc6701427b2", size = 4023334, upload-time = "2025-05-01T16:35:48.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/be/7b423b02b09eb856beffe76fe8c4121c99852db74dd12a422dcb72d1134e/nox-2025.5.1-py3-none-any.whl", hash = "sha256:56abd55cf37ff523c254fcec4d152ed51e5fe80e2ab8317221d8b828ac970a31", size = 71753, upload-time = "2025-05-01T16:35:46.037Z" }, +] + +[[package]] +name = "nox-uv" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nox" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/18/726fc3942a5d2a6a8686d0b220aa12e8459e4294181c7842715e7dc0c2db/nox_uv-0.6.2.tar.gz", hash = "sha256:a7a0ffa9a868f48cf919e09f18fb266a89d5cac69a69ce1799fcfd6a7ba09285", size = 4937, upload-time = "2025-07-31T00:28:44.719Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/7c/b6d82aa05f9790846733cd7d5696641f5bdf8704d267a9ccb9e92b977f9d/nox_uv-0.6.2-py3-none-any.whl", hash = "sha256:705b3054d0f3f8134872f03e01ea29ee35d5c8a8a7085b3751306c82d35d4b6d", size = 5335, upload-time = "2025-07-31T00:28:43.961Z" }, +] + +[[package]] +name = "openpyxl" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "et-xmlfile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, +] + +[[package]] +name = "packageurl-python" +version = "0.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/0f/66c682b6d6844247a5ba04e1be51ee782d1a921ebffc8fa0b3f4d520d885/packageurl_python-0.17.3.tar.gz", hash = "sha256:719995f0c7f706890277ba57ec95afcaa9696c836a7675770a1279b01a41f7be", size = 43004, upload-time = "2025-08-01T03:24:35.307Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/39/71ca0df9154070661a967edc2a518d612e94782cfe502ab09f180efc224c/packageurl_python-0.17.3-py3-none-any.whl", hash = "sha256:f51b5aab570159f07258c8e998e9972ff3bf060da16b7334a42bd9f9737777d9", size = 29942, upload-time = "2025-08-01T03:24:33.131Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "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 = "pip" +version = "25.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/16/650289cd3f43d5a2fadfd98c68bd1e1e7f2550a1a5326768cddfbcedb2c5/pip-25.2.tar.gz", hash = "sha256:578283f006390f85bb6282dffb876454593d637f5d1be494b5202ce4877e71f2", size = 1840021, upload-time = "2025-07-30T21:50:15.401Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/3f/945ef7ab14dc4f9d7f40288d2df998d1837ee0888ec3659c813487572faa/pip-25.2-py3-none-any.whl", hash = "sha256:6d67a2b4e7f14d8b31b8b52648866fa717f45a1eb70e83002f4331d07e953717", size = 1752557, upload-time = "2025-07-30T21:50:13.323Z" }, +] + +[[package]] +name = "pip-api" +version = "0.0.34" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pip" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/f1/ee85f8c7e82bccf90a3c7aad22863cc6e20057860a1361083cd2adacb92e/pip_api-0.0.34.tar.gz", hash = "sha256:9b75e958f14c5a2614bae415f2adf7eeb54d50a2cfbe7e24fd4826471bac3625", size = 123017, upload-time = "2024-07-09T20:32:30.641Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/f7/ebf5003e1065fd00b4cbef53bf0a65c3d3e1b599b676d5383ccb7a8b88ba/pip_api-0.0.34-py3-none-any.whl", hash = "sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb", size = 120369, upload-time = "2024-07-09T20:32:29.099Z" }, +] + +[[package]] +name = "pip-audit" +version = "2.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachecontrol", extra = ["filecache"] }, + { name = "cyclonedx-python-lib" }, + { name = "packaging" }, + { name = "pip-api" }, + { name = "pip-requirements-parser" }, + { name = "platformdirs" }, + { name = "requests" }, + { name = "rich" }, + { name = "toml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/7f/28fad19a9806f796f13192ab6974c07c4a04d9cbb8e30dd895c3c11ce7ee/pip_audit-2.9.0.tar.gz", hash = "sha256:0b998410b58339d7a231e5aa004326a294e4c7c6295289cdc9d5e1ef07b1f44d", size = 52089, upload-time = "2025-04-07T16:45:23.679Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/9e/f4dfd9d3dadb6d6dc9406f1111062f871e2e248ed7b584cca6020baf2ac1/pip_audit-2.9.0-py3-none-any.whl", hash = "sha256:348b16e60895749a0839875d7cc27ebd692e1584ebe5d5cb145941c8e25a80bd", size = 58634, upload-time = "2025-04-07T16:45:22.056Z" }, +] + +[[package]] +name = "pip-requirements-parser" +version = "32.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/2a/63b574101850e7f7b306ddbdb02cb294380d37948140eecd468fae392b54/pip-requirements-parser-32.0.1.tar.gz", hash = "sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3", size = 209359, upload-time = "2022-12-21T15:25:22.732Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/d0/d04f1d1e064ac901439699ee097f58688caadea42498ec9c4b4ad2ef84ab/pip_requirements_parser-32.0.1-py3-none-any.whl", hash = "sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526", size = 35648, upload-time = "2022-12-21T15:25:21.046Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, +] + +[[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 = "pre-commit" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424, upload-time = "2025-03-18T21:35:20.987Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" }, +] + +[[package]] +name = "pre-commit-uv" +version = "4.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pre-commit" }, + { name = "uv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/6c/c3c1d01698c8abb0b546defc0304971fa7fb2ba84ad35587b9dad095d73f/pre_commit_uv-4.1.4.tar.gz", hash = "sha256:3db606a79b226127b27dbbd8381b78c0e30de3ac775a8492c576a68e9250535c", size = 6493, upload-time = "2024-10-29T23:07:28.918Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/70/1b65f9118ef64f6ffe5d57a67170bbff25d4f4a3d1cb78e8ed3392e16114/pre_commit_uv-4.1.4-py3-none-any.whl", hash = "sha256:7f01fb494fa1caa5097d20a38f71df7cea0209197b2564699cef9b3f3aa9d135", size = 5578, upload-time = "2024-10-29T23:07:27.128Z" }, +] + +[[package]] +name = "psycopg" +version = "3.2.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/4a/93a6ab570a8d1a4ad171a1f4256e205ce48d828781312c0bbaff36380ecb/psycopg-3.2.9.tar.gz", hash = "sha256:2fbb46fcd17bc81f993f28c47f1ebea38d66ae97cc2dbc3cad73b37cefbff700", size = 158122, upload-time = "2025-05-13T16:11:15.533Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/b0/a73c195a56eb6b92e937a5ca58521a5c3346fb233345adc80fd3e2f542e2/psycopg-3.2.9-py3-none-any.whl", hash = "sha256:01a8dadccdaac2123c916208c96e06631641c0566b22005493f09663c7a8d3b6", size = 202705, upload-time = "2025-05-13T16:06:26.584Z" }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.2.9" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/ce/d677bc51f9b180986e5515268603519cee682eb6b5e765ae46cdb8526579/psycopg_binary-3.2.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:528239bbf55728ba0eacbd20632342867590273a9bacedac7538ebff890f1093", size = 4033081, upload-time = "2025-05-13T16:06:29.666Z" }, + { url = "https://files.pythonhosted.org/packages/de/f4/b56263eb20dc36d71d7188622872098400536928edf86895736e28546b3c/psycopg_binary-3.2.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4978c01ca4c208c9d6376bd585e2c0771986b76ff7ea518f6d2b51faece75e8", size = 4082141, upload-time = "2025-05-13T16:06:33.81Z" }, + { url = "https://files.pythonhosted.org/packages/68/47/5316c3b0a2b1ff5f1d440a27638250569994534874a2ce88bf24f5c51c0f/psycopg_binary-3.2.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ed2bab85b505d13e66a914d0f8cdfa9475c16d3491cf81394e0748b77729af2", size = 4678993, upload-time = "2025-05-13T16:06:36.309Z" }, + { url = "https://files.pythonhosted.org/packages/53/24/b2c667b59f07fd7d7805c0c2074351bf2b98a336c5030d961db316512ffb/psycopg_binary-3.2.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:799fa1179ab8a58d1557a95df28b492874c8f4135101b55133ec9c55fc9ae9d7", size = 4500117, upload-time = "2025-05-13T16:06:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/ae/91/a08f8878b0fe0b34b083c149df950bce168bc1b18b2fe849fa42bf4378d4/psycopg_binary-3.2.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb37ac3955d19e4996c3534abfa4f23181333974963826db9e0f00731274b695", size = 4766985, upload-time = "2025-05-13T16:06:42.502Z" }, + { url = "https://files.pythonhosted.org/packages/10/be/3a45d5b7d8f4c4332fd42465f2170b5aef4d28a7c79e79ac7e5e1dac74d7/psycopg_binary-3.2.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:001e986656f7e06c273dd4104e27f4b4e0614092e544d950c7c938d822b1a894", size = 4461990, upload-time = "2025-05-13T16:06:45.971Z" }, + { url = "https://files.pythonhosted.org/packages/03/ce/20682b9a4fc270d8dc644a0b16c1978732146c6ff0abbc48fbab2f4a70aa/psycopg_binary-3.2.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fa5c80d8b4cbf23f338db88a7251cef8bb4b68e0f91cf8b6ddfa93884fdbb0c1", size = 3777947, upload-time = "2025-05-13T16:06:49.134Z" }, + { url = "https://files.pythonhosted.org/packages/07/5c/f6d486e00bcd8709908ccdd436b2a190d390dfd61e318de4060bc6ee2a1e/psycopg_binary-3.2.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:39a127e0cf9b55bd4734a8008adf3e01d1fd1cb36339c6a9e2b2cbb6007c50ee", size = 3337502, upload-time = "2025-05-13T16:06:51.378Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a1/086508e929c0123a7f532840bb0a0c8a1ebd7e06aef3ee7fa44a3589bcdf/psycopg_binary-3.2.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fb7599e436b586e265bea956751453ad32eb98be6a6e694252f4691c31b16edb", size = 3440809, upload-time = "2025-05-13T16:06:54.552Z" }, + { url = "https://files.pythonhosted.org/packages/40/f2/3a347a0f894355a6b173fca2202eca279b6197727b24e4896cf83f4263ee/psycopg_binary-3.2.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5d2c9fe14fe42b3575a0b4e09b081713e83b762c8dc38a3771dd3265f8f110e7", size = 3497231, upload-time = "2025-05-13T16:06:58.858Z" }, + { url = "https://files.pythonhosted.org/packages/18/31/0845a385eb6f4521b398793293b5f746a101e80d5c43792990442d26bc2e/psycopg_binary-3.2.9-cp310-cp310-win_amd64.whl", hash = "sha256:7e4660fad2807612bb200de7262c88773c3483e85d981324b3c647176e41fdc8", size = 2936845, upload-time = "2025-05-13T16:07:02.712Z" }, + { url = "https://files.pythonhosted.org/packages/b6/84/259ea58aca48e03c3c793b4ccfe39ed63db7b8081ef784d039330d9eed96/psycopg_binary-3.2.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2504e9fd94eabe545d20cddcc2ff0da86ee55d76329e1ab92ecfcc6c0a8156c4", size = 4040785, upload-time = "2025-05-13T16:07:07.569Z" }, + { url = "https://files.pythonhosted.org/packages/25/22/ce58ffda2b7e36e45042b4d67f1bbd4dd2ccf4cfd2649696685c61046475/psycopg_binary-3.2.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:093a0c079dd6228a7f3c3d82b906b41964eaa062a9a8c19f45ab4984bf4e872b", size = 4087601, upload-time = "2025-05-13T16:07:11.75Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4f/b043e85268650c245025e80039b79663d8986f857bc3d3a72b1de67f3550/psycopg_binary-3.2.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:387c87b51d72442708e7a853e7e7642717e704d59571da2f3b29e748be58c78a", size = 4676524, upload-time = "2025-05-13T16:07:17.038Z" }, + { url = "https://files.pythonhosted.org/packages/da/29/7afbfbd3740ea52fda488db190ef2ef2a9ff7379b85501a2142fb9f7dd56/psycopg_binary-3.2.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9ac10a2ebe93a102a326415b330fff7512f01a9401406896e78a81d75d6eddc", size = 4495671, upload-time = "2025-05-13T16:07:21.709Z" }, + { url = "https://files.pythonhosted.org/packages/ea/eb/df69112d18a938cbb74efa1573082248437fa663ba66baf2cdba8a95a2d0/psycopg_binary-3.2.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72fdbda5b4c2a6a72320857ef503a6589f56d46821592d4377c8c8604810342b", size = 4768132, upload-time = "2025-05-13T16:07:25.818Z" }, + { url = "https://files.pythonhosted.org/packages/76/fe/4803b20220c04f508f50afee9169268553f46d6eed99640a08c8c1e76409/psycopg_binary-3.2.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f34e88940833d46108f949fdc1fcfb74d6b5ae076550cd67ab59ef47555dba95", size = 4458394, upload-time = "2025-05-13T16:07:29.148Z" }, + { url = "https://files.pythonhosted.org/packages/0f/0f/5ecc64607ef6f62b04e610b7837b1a802ca6f7cb7211339f5d166d55f1dd/psycopg_binary-3.2.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a3e0f89fe35cb03ff1646ab663dabf496477bab2a072315192dbaa6928862891", size = 3776879, upload-time = "2025-05-13T16:07:32.503Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d8/1c3d6e99b7db67946d0eac2cd15d10a79aa7b1e3222ce4aa8e7df72027f5/psycopg_binary-3.2.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6afb3e62f2a3456f2180a4eef6b03177788df7ce938036ff7f09b696d418d186", size = 3333329, upload-time = "2025-05-13T16:07:35.555Z" }, + { url = "https://files.pythonhosted.org/packages/d7/02/a4e82099816559f558ccaf2b6945097973624dc58d5d1c91eb1e54e5a8e9/psycopg_binary-3.2.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:cc19ed5c7afca3f6b298bfc35a6baa27adb2019670d15c32d0bb8f780f7d560d", size = 3435683, upload-time = "2025-05-13T16:07:37.863Z" }, + { url = "https://files.pythonhosted.org/packages/91/e4/f27055290d58e8818bed8a297162a096ef7f8ecdf01d98772d4b02af46c4/psycopg_binary-3.2.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc75f63653ce4ec764c8f8c8b0ad9423e23021e1c34a84eb5f4ecac8538a4a4a", size = 3497124, upload-time = "2025-05-13T16:07:40.567Z" }, + { url = "https://files.pythonhosted.org/packages/67/3d/17ed07579625529534605eeaeba34f0536754a5667dbf20ea2624fc80614/psycopg_binary-3.2.9-cp311-cp311-win_amd64.whl", hash = "sha256:3db3ba3c470801e94836ad78bf11fd5fab22e71b0c77343a1ee95d693879937a", size = 2939520, upload-time = "2025-05-13T16:07:45.467Z" }, + { url = "https://files.pythonhosted.org/packages/29/6f/ec9957e37a606cd7564412e03f41f1b3c3637a5be018d0849914cb06e674/psycopg_binary-3.2.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be7d650a434921a6b1ebe3fff324dbc2364393eb29d7672e638ce3e21076974e", size = 4022205, upload-time = "2025-05-13T16:07:48.195Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ba/497b8bea72b20a862ac95a94386967b745a472d9ddc88bc3f32d5d5f0d43/psycopg_binary-3.2.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6a76b4722a529390683c0304501f238b365a46b1e5fb6b7249dbc0ad6fea51a0", size = 4083795, upload-time = "2025-05-13T16:07:50.917Z" }, + { url = "https://files.pythonhosted.org/packages/42/07/af9503e8e8bdad3911fd88e10e6a29240f9feaa99f57d6fac4a18b16f5a0/psycopg_binary-3.2.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96a551e4683f1c307cfc3d9a05fec62c00a7264f320c9962a67a543e3ce0d8ff", size = 4655043, upload-time = "2025-05-13T16:07:54.857Z" }, + { url = "https://files.pythonhosted.org/packages/28/ed/aff8c9850df1648cc6a5cc7a381f11ee78d98a6b807edd4a5ae276ad60ad/psycopg_binary-3.2.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:61d0a6ceed8f08c75a395bc28cb648a81cf8dee75ba4650093ad1a24a51c8724", size = 4477972, upload-time = "2025-05-13T16:07:57.925Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/8e9d1b77ec1a632818fe2f457c3a65af83c68710c4c162d6866947d08cc5/psycopg_binary-3.2.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad280bbd409bf598683dda82232f5215cfc5f2b1bf0854e409b4d0c44a113b1d", size = 4737516, upload-time = "2025-05-13T16:08:01.616Z" }, + { url = "https://files.pythonhosted.org/packages/46/ec/222238f774cd5a0881f3f3b18fb86daceae89cc410f91ef6a9fb4556f236/psycopg_binary-3.2.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76eddaf7fef1d0994e3d536ad48aa75034663d3a07f6f7e3e601105ae73aeff6", size = 4436160, upload-time = "2025-05-13T16:08:04.278Z" }, + { url = "https://files.pythonhosted.org/packages/37/78/af5af2a1b296eeca54ea7592cd19284739a844974c9747e516707e7b3b39/psycopg_binary-3.2.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:52e239cd66c4158e412318fbe028cd94b0ef21b0707f56dcb4bdc250ee58fd40", size = 3753518, upload-time = "2025-05-13T16:08:07.567Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ac/8a3ed39ea069402e9e6e6a2f79d81a71879708b31cc3454283314994b1ae/psycopg_binary-3.2.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:08bf9d5eabba160dd4f6ad247cf12f229cc19d2458511cab2eb9647f42fa6795", size = 3313598, upload-time = "2025-05-13T16:08:09.999Z" }, + { url = "https://files.pythonhosted.org/packages/da/43/26549af068347c808fbfe5f07d2fa8cef747cfff7c695136172991d2378b/psycopg_binary-3.2.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1b2cf018168cad87580e67bdde38ff5e51511112f1ce6ce9a8336871f465c19a", size = 3407289, upload-time = "2025-05-13T16:08:12.66Z" }, + { url = "https://files.pythonhosted.org/packages/67/55/ea8d227c77df8e8aec880ded398316735add8fda5eb4ff5cc96fac11e964/psycopg_binary-3.2.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:14f64d1ac6942ff089fc7e926440f7a5ced062e2ed0949d7d2d680dc5c00e2d4", size = 3472493, upload-time = "2025-05-13T16:08:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/3c/02/6ff2a5bc53c3cd653d281666728e29121149179c73fddefb1e437024c192/psycopg_binary-3.2.9-cp312-cp312-win_amd64.whl", hash = "sha256:7a838852e5afb6b4126f93eb409516a8c02a49b788f4df8b6469a40c2157fa21", size = 2927400, upload-time = "2025-05-13T16:08:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/28/0b/f61ff4e9f23396aca674ed4d5c9a5b7323738021d5d72d36d8b865b3deaf/psycopg_binary-3.2.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:98bbe35b5ad24a782c7bf267596638d78aa0e87abc7837bdac5b2a2ab954179e", size = 4017127, upload-time = "2025-05-13T16:08:21.391Z" }, + { url = "https://files.pythonhosted.org/packages/bc/00/7e181fb1179fbfc24493738b61efd0453d4b70a0c4b12728e2b82db355fd/psycopg_binary-3.2.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:72691a1615ebb42da8b636c5ca9f2b71f266be9e172f66209a361c175b7842c5", size = 4080322, upload-time = "2025-05-13T16:08:24.049Z" }, + { url = "https://files.pythonhosted.org/packages/58/fd/94fc267c1d1392c4211e54ccb943be96ea4032e761573cf1047951887494/psycopg_binary-3.2.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25ab464bfba8c401f5536d5aa95f0ca1dd8257b5202eede04019b4415f491351", size = 4655097, upload-time = "2025-05-13T16:08:27.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/17/31b3acf43de0b2ba83eac5878ff0dea5a608ca2a5c5dd48067999503a9de/psycopg_binary-3.2.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e8aeefebe752f46e3c4b769e53f1d4ad71208fe1150975ef7662c22cca80fab", size = 4482114, upload-time = "2025-05-13T16:08:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/85/78/b4d75e5fd5a85e17f2beb977abbba3389d11a4536b116205846b0e1cf744/psycopg_binary-3.2.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7e4e4dd177a8665c9ce86bc9caae2ab3aa9360b7ce7ec01827ea1baea9ff748", size = 4737693, upload-time = "2025-05-13T16:08:34.625Z" }, + { url = "https://files.pythonhosted.org/packages/3b/95/7325a8550e3388b00b5e54f4ced5e7346b531eb4573bf054c3dbbfdc14fe/psycopg_binary-3.2.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fc2915949e5c1ea27a851f7a472a7da7d0a40d679f0a31e42f1022f3c562e87", size = 4437423, upload-time = "2025-05-13T16:08:37.444Z" }, + { url = "https://files.pythonhosted.org/packages/1a/db/cef77d08e59910d483df4ee6da8af51c03bb597f500f1fe818f0f3b925d3/psycopg_binary-3.2.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a1fa38a4687b14f517f049477178093c39c2a10fdcced21116f47c017516498f", size = 3758667, upload-time = "2025-05-13T16:08:40.116Z" }, + { url = "https://files.pythonhosted.org/packages/95/3e/252fcbffb47189aa84d723b54682e1bb6d05c8875fa50ce1ada914ae6e28/psycopg_binary-3.2.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5be8292d07a3ab828dc95b5ee6b69ca0a5b2e579a577b39671f4f5b47116dfd2", size = 3320576, upload-time = "2025-05-13T16:08:43.243Z" }, + { url = "https://files.pythonhosted.org/packages/1c/cd/9b5583936515d085a1bec32b45289ceb53b80d9ce1cea0fef4c782dc41a7/psycopg_binary-3.2.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:778588ca9897b6c6bab39b0d3034efff4c5438f5e3bd52fda3914175498202f9", size = 3411439, upload-time = "2025-05-13T16:08:47.321Z" }, + { url = "https://files.pythonhosted.org/packages/45/6b/6f1164ea1634c87956cdb6db759e0b8c5827f989ee3cdff0f5c70e8331f2/psycopg_binary-3.2.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f0d5b3af045a187aedbd7ed5fc513bd933a97aaff78e61c3745b330792c4345b", size = 3477477, upload-time = "2025-05-13T16:08:51.166Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1d/bf54cfec79377929da600c16114f0da77a5f1670f45e0c3af9fcd36879bc/psycopg_binary-3.2.9-cp313-cp313-win_amd64.whl", hash = "sha256:2290bc146a1b6a9730350f695e8b670e1d1feb8446597bed0bbe7c3c30e0abcb", size = 2928009, upload-time = "2025-05-13T16:08:53.67Z" }, +] + +[[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 = "py-serializable" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "defusedxml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/21/d250cfca8ff30c2e5a7447bc13861541126ce9bd4426cd5d0c9f08b5547d/py_serializable-2.1.0.tar.gz", hash = "sha256:9d5db56154a867a9b897c0163b33a793c804c80cee984116d02d49e4578fc103", size = 52368, upload-time = "2025-07-21T09:56:48.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/bf/7595e817906a29453ba4d99394e781b6fabe55d21f3c15d240f85dd06bb1/py_serializable-2.1.0-py3-none-any.whl", hash = "sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304", size = 23045, upload-time = "2025-07-21T09:56:46.848Z" }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyodbc" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a0/36/a1ac7d23a1611e7ccd4d27df096f3794e8d1e7faa040260d9d41b6fc3185/pyodbc-5.2.0.tar.gz", hash = "sha256:de8be39809c8ddeeee26a4b876a6463529cd487a60d1393eb2a93e9bcd44a8f5", size = 116908, upload-time = "2024-10-16T01:40:13.425Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/01/05c4a4ec122c4a8a37fa1be5bdbf6fb23724a2ee3b1b771bb46f710158a9/pyodbc-5.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eb0850e3e3782f57457feed297e220bb20c3e8fd7550d7a6b6bb96112bd9b6fe", size = 72483, upload-time = "2024-10-16T01:39:23.697Z" }, + { url = "https://files.pythonhosted.org/packages/73/22/ba718cc5508bdfbb53e1906018d7f597be37241c769dda8a48f52af96fe3/pyodbc-5.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0dae0fb86078c87acf135dbe5afd3c7d15d52ab0db5965c44159e84058c3e2fb", size = 71794, upload-time = "2024-10-16T01:39:25.372Z" }, + { url = "https://files.pythonhosted.org/packages/24/e4/9d859ea3642059c10a6644a00ccb1f8b8e02c1e4f49ab34250db1273c2c5/pyodbc-5.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6493b9c7506ca964b80ad638d0dc82869df7058255d71f04fdd1405e88bcb36b", size = 332850, upload-time = "2024-10-16T01:39:27.789Z" }, + { url = "https://files.pythonhosted.org/packages/b9/a7/98c3555c10cfeb343ec7eea69ecb17476aa3ace72131ea8a4a1f8250318c/pyodbc-5.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e04de873607fb960e71953c164c83e8e5d9291ce0d69e688e54947b254b04902", size = 336009, upload-time = "2024-10-16T01:39:29.694Z" }, + { url = "https://files.pythonhosted.org/packages/24/c1/d5b16dd62eb70f281bc90cdc1e3c46af7acda3f0f6afb34553206506ccb2/pyodbc-5.2.0-cp310-cp310-win32.whl", hash = "sha256:74135cb10c1dcdbd99fe429c61539c232140e62939fa7c69b0a373cc552e4a08", size = 62407, upload-time = "2024-10-16T01:39:31.894Z" }, + { url = "https://files.pythonhosted.org/packages/f5/12/22c83669abee4ca5915aa89172cf1673b58ca05f44dabeb8b0bac9b7fecc/pyodbc-5.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:d287121eeaa562b9ab3d4c52fa77c793dfedd127049273eb882a05d3d67a8ce8", size = 68874, upload-time = "2024-10-16T01:39:33.325Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a2/5907ce319a571eb1e271d6a475920edfeacd92da1021bb2a15ed1b7f6ac1/pyodbc-5.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4627779f0a608b51ce2d2fe6d1d395384e65ca36248bf9dbb6d7cf2c8fda1cab", size = 72536, upload-time = "2024-10-16T01:39:34.715Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b8/bd438ab2bb9481615142784b0c9778079a87ae1bca7a0fe8aabfc088aa9f/pyodbc-5.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d997d3b6551273647825c734158ca8a6f682df269f6b3975f2499c01577ddec", size = 71825, upload-time = "2024-10-16T01:39:36.343Z" }, + { url = "https://files.pythonhosted.org/packages/8b/82/cf71ae99b511a7f20c380ce470de233a0291fa3798afa74e0adc8fad1675/pyodbc-5.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5102007a8c78dd2fc1c1b6f6147de8cfc020f81013e4b46c33e66aaa7d1bf7b1", size = 342304, upload-time = "2024-10-16T01:39:37.82Z" }, + { url = "https://files.pythonhosted.org/packages/43/ea/03fe042f4a390df05e753ddd21c6cab006baae1eee71ce230f6e2a883944/pyodbc-5.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e3cbc7075a46c411b531ada557c4aef13d034060a70077717124cabc1717e2d", size = 346186, upload-time = "2024-10-16T01:39:39.3Z" }, + { url = "https://files.pythonhosted.org/packages/f9/80/48178bb50990147adb72ec9e377e94517a0dfaf2f2a6e3fe477d9a33671f/pyodbc-5.2.0-cp311-cp311-win32.whl", hash = "sha256:de1ee7ec2eb326b7be5e2c4ce20d472c5ef1a6eb838d126d1d26779ff5486e49", size = 62418, upload-time = "2024-10-16T01:39:40.797Z" }, + { url = "https://files.pythonhosted.org/packages/7c/6b/f0ad7d8a535d58f35f375ffbf367c68d0ec54452a431d23b0ebee4cd44c6/pyodbc-5.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:113f904b9852c12f10c7a3288f5a3563ecdbbefe3ccc829074a9eb8255edcd29", size = 68871, upload-time = "2024-10-16T01:39:41.997Z" }, + { url = "https://files.pythonhosted.org/packages/26/26/104525b728fedfababd3143426b9d0008c70f0d604a3bf5d4773977d83f4/pyodbc-5.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be43d1ece4f2cf4d430996689d89a1a15aeb3a8da8262527e5ced5aee27e89c3", size = 73014, upload-time = "2024-10-16T01:39:43.332Z" }, + { url = "https://files.pythonhosted.org/packages/4f/7d/bb632488b603bcd2a6753b858e8bc7dd56146dd19bd72003cc09ae6e3fc0/pyodbc-5.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9f7badd0055221a744d76c11440c0856fd2846ed53b6555cf8f0a8893a3e4b03", size = 72515, upload-time = "2024-10-16T01:39:44.506Z" }, + { url = "https://files.pythonhosted.org/packages/ab/38/a1b9bfe5a7062672268553c2d6ff93676173b0fb4bd583e8c4f74a0e296f/pyodbc-5.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad633c52f4f4e7691daaa2278d6e6ebb2fe4ae7709e610e22c7dd1a1d620cf8b", size = 348561, upload-time = "2024-10-16T01:39:45.986Z" }, + { url = "https://files.pythonhosted.org/packages/71/82/ddb1c41c682550116f391aa6cab2052910046a30d63014bbe6d09c4958f4/pyodbc-5.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97d086a8f7a302b74c9c2e77bedf954a603b19168af900d4d3a97322e773df63", size = 353962, upload-time = "2024-10-16T01:39:47.254Z" }, + { url = "https://files.pythonhosted.org/packages/e5/29/fec0e739d0c1cab155843ed71d0717f5e1694effe3f28d397168f48bcd92/pyodbc-5.2.0-cp312-cp312-win32.whl", hash = "sha256:0e4412f8e608db2a4be5bcc75f9581f386ed6a427dbcb5eac795049ba6fc205e", size = 63050, upload-time = "2024-10-16T01:39:48.8Z" }, + { url = "https://files.pythonhosted.org/packages/21/7f/3a47e022a97b017ffb73351a1061e4401bcb5aa4fc0162d04f4e5452e4fc/pyodbc-5.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:b1f5686b142759c5b2bdbeaa0692622c2ebb1f10780eb3c174b85f5607fbcf55", size = 69485, upload-time = "2024-10-16T01:39:49.732Z" }, + { url = "https://files.pythonhosted.org/packages/90/be/e5f8022ec57a7ea6aa3717a3f307a44c3b012fce7ad6ec91aad3e2a56978/pyodbc-5.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:26844d780045bbc3514d5c2f0d89e7fda7df7db0bd24292eb6902046f5730885", size = 72982, upload-time = "2024-10-16T01:39:50.738Z" }, + { url = "https://files.pythonhosted.org/packages/5c/0e/71111e4f53936b0b99731d9b6acfc8fc95660533a1421447a63d6e519112/pyodbc-5.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:26d2d8fd53b71204c755abc53b0379df4e23fd9a40faf211e1cb87e8a32470f0", size = 72515, upload-time = "2024-10-16T01:39:51.86Z" }, + { url = "https://files.pythonhosted.org/packages/a5/09/3c06bbc1ebb9ae15f53cefe10774809b67da643883287ba1c44ba053816a/pyodbc-5.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a27996b6d27e275dfb5fe8a34087ba1cacadfd1439e636874ef675faea5149d9", size = 347470, upload-time = "2024-10-16T01:39:53.594Z" }, + { url = "https://files.pythonhosted.org/packages/a4/35/1c7efd4665e7983169d20175014f68578e0edfcbc4602b0bafcefa522c4a/pyodbc-5.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaf42c4bd323b8fd01f1cd900cca2d09232155f9b8f0b9bcd0be66763588ce64", size = 353025, upload-time = "2024-10-16T01:39:55.124Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c9/736d07fa33572abdc50d858fd9e527d2c8281f3acbb90dff4999a3662edd/pyodbc-5.2.0-cp313-cp313-win32.whl", hash = "sha256:207f16b7e9bf09c591616429ebf2b47127e879aad21167ac15158910dc9bbcda", size = 63052, upload-time = "2024-10-16T01:39:56.565Z" }, + { url = "https://files.pythonhosted.org/packages/73/2a/3219c8b7fa3788fc9f27b5fc2244017223cf070e5ab370f71c519adf9120/pyodbc-5.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:96d3127f28c0dacf18da7ae009cd48eac532d3dcc718a334b86a3c65f6a5ef5c", size = 69486, upload-time = "2024-10-16T01:39:57.57Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608, upload-time = "2025-03-25T05:01:28.114Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" }, +] + +[[package]] +name = "pyquery" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cssselect" }, + { name = "lxml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/48/79e774ea00b671d08867f06d9258203be81834236c150ac00e942d8fc4db/pyquery-2.0.1.tar.gz", hash = "sha256:0194bb2706b12d037db12c51928fe9ebb36b72d9e719565daba5a6c595322faf", size = 44999, upload-time = "2024-08-30T08:12:24.289Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/f5/5067b48012967ea166b9bd0a015b69e0560e4c6e7c06f28d9bab8f9dd10b/pyquery-2.0.1-py3-none-any.whl", hash = "sha256:aedfa0bd0eb9afc94b3ddbec8f375a6362b32bc9662f46e3e0d866483f4771b0", size = 22573, upload-time = "2024-08-30T08:12:22.586Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, +] + +[[package]] +name = "pytest-cov" +version = "6.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, +] + +[[package]] +name = "rich" +version = "14.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, +] + +[[package]] +name = "ruff" +version = "0.12.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/81/0bd3594fa0f690466e41bd033bdcdf86cba8288345ac77ad4afbe5ec743a/ruff-0.12.7.tar.gz", hash = "sha256:1fc3193f238bc2d7968772c82831a4ff69252f673be371fb49663f0068b7ec71", size = 5197814, upload-time = "2025-07-29T22:32:35.877Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/d2/6cb35e9c85e7a91e8d22ab32ae07ac39cc34a71f1009a6f9e4a2a019e602/ruff-0.12.7-py3-none-linux_armv6l.whl", hash = "sha256:76e4f31529899b8c434c3c1dede98c4483b89590e15fb49f2d46183801565303", size = 11852189, upload-time = "2025-07-29T22:31:41.281Z" }, + { url = "https://files.pythonhosted.org/packages/63/5b/a4136b9921aa84638f1a6be7fb086f8cad0fde538ba76bda3682f2599a2f/ruff-0.12.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:789b7a03e72507c54fb3ba6209e4bb36517b90f1a3569ea17084e3fd295500fb", size = 12519389, upload-time = "2025-07-29T22:31:54.265Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c9/3e24a8472484269b6b1821794141f879c54645a111ded4b6f58f9ab0705f/ruff-0.12.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e1c2a3b8626339bb6369116e7030a4cf194ea48f49b64bb505732a7fce4f4e3", size = 11743384, upload-time = "2025-07-29T22:31:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/26/7c/458dd25deeb3452c43eaee853c0b17a1e84169f8021a26d500ead77964fd/ruff-0.12.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32dec41817623d388e645612ec70d5757a6d9c035f3744a52c7b195a57e03860", size = 11943759, upload-time = "2025-07-29T22:32:01.95Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8b/658798472ef260ca050e400ab96ef7e85c366c39cf3dfbef4d0a46a528b6/ruff-0.12.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47ef751f722053a5df5fa48d412dbb54d41ab9b17875c6840a58ec63ff0c247c", size = 11654028, upload-time = "2025-07-29T22:32:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/a8/86/9c2336f13b2a3326d06d39178fd3448dcc7025f82514d1b15816fe42bfe8/ruff-0.12.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a828a5fc25a3efd3e1ff7b241fd392686c9386f20e5ac90aa9234a5faa12c423", size = 13225209, upload-time = "2025-07-29T22:32:06.952Z" }, + { url = "https://files.pythonhosted.org/packages/76/69/df73f65f53d6c463b19b6b312fd2391dc36425d926ec237a7ed028a90fc1/ruff-0.12.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5726f59b171111fa6a69d82aef48f00b56598b03a22f0f4170664ff4d8298efb", size = 14182353, upload-time = "2025-07-29T22:32:10.053Z" }, + { url = "https://files.pythonhosted.org/packages/58/1e/de6cda406d99fea84b66811c189b5ea139814b98125b052424b55d28a41c/ruff-0.12.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74e6f5c04c4dd4aba223f4fe6e7104f79e0eebf7d307e4f9b18c18362124bccd", size = 13631555, upload-time = "2025-07-29T22:32:12.644Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ae/625d46d5164a6cc9261945a5e89df24457dc8262539ace3ac36c40f0b51e/ruff-0.12.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d0bfe4e77fba61bf2ccadf8cf005d6133e3ce08793bbe870dd1c734f2699a3e", size = 12667556, upload-time = "2025-07-29T22:32:15.312Z" }, + { url = "https://files.pythonhosted.org/packages/55/bf/9cb1ea5e3066779e42ade8d0cd3d3b0582a5720a814ae1586f85014656b6/ruff-0.12.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06bfb01e1623bf7f59ea749a841da56f8f653d641bfd046edee32ede7ff6c606", size = 12939784, upload-time = "2025-07-29T22:32:17.69Z" }, + { url = "https://files.pythonhosted.org/packages/55/7f/7ead2663be5627c04be83754c4f3096603bf5e99ed856c7cd29618c691bd/ruff-0.12.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e41df94a957d50083fd09b916d6e89e497246698c3f3d5c681c8b3e7b9bb4ac8", size = 11771356, upload-time = "2025-07-29T22:32:20.134Z" }, + { url = "https://files.pythonhosted.org/packages/17/40/a95352ea16edf78cd3a938085dccc55df692a4d8ba1b3af7accbe2c806b0/ruff-0.12.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4000623300563c709458d0ce170c3d0d788c23a058912f28bbadc6f905d67afa", size = 11612124, upload-time = "2025-07-29T22:32:22.645Z" }, + { url = "https://files.pythonhosted.org/packages/4d/74/633b04871c669e23b8917877e812376827c06df866e1677f15abfadc95cb/ruff-0.12.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:69ffe0e5f9b2cf2b8e289a3f8945b402a1b19eff24ec389f45f23c42a3dd6fb5", size = 12479945, upload-time = "2025-07-29T22:32:24.765Z" }, + { url = "https://files.pythonhosted.org/packages/be/34/c3ef2d7799c9778b835a76189c6f53c179d3bdebc8c65288c29032e03613/ruff-0.12.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a07a5c8ffa2611a52732bdc67bf88e243abd84fe2d7f6daef3826b59abbfeda4", size = 12998677, upload-time = "2025-07-29T22:32:27.022Z" }, + { url = "https://files.pythonhosted.org/packages/77/ab/aca2e756ad7b09b3d662a41773f3edcbd262872a4fc81f920dc1ffa44541/ruff-0.12.7-py3-none-win32.whl", hash = "sha256:c928f1b2ec59fb77dfdf70e0419408898b63998789cc98197e15f560b9e77f77", size = 11756687, upload-time = "2025-07-29T22:32:29.381Z" }, + { url = "https://files.pythonhosted.org/packages/b4/71/26d45a5042bc71db22ddd8252ca9d01e9ca454f230e2996bb04f16d72799/ruff-0.12.7-py3-none-win_amd64.whl", hash = "sha256:9c18f3d707ee9edf89da76131956aba1270c6348bfee8f6c647de841eac7194f", size = 12912365, upload-time = "2025-07-29T22:32:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/4c/9b/0b8aa09817b63e78d94b4977f18b1fcaead3165a5ee49251c5d5c245bb2d/ruff-0.12.7-py3-none-win_arm64.whl", hash = "sha256:dfce05101dbd11833a0776716d5d1578641b7fddb537fe7fa956ab85d1769b69", size = 11982083, upload-time = "2025-07-29T22:32:33.881Z" }, +] + +[[package]] +name = "secretstorage" +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739, upload-time = "2022-08-13T16:22:46.976Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221, upload-time = "2022-08-13T16:22:44.457Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[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 = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload-time = "2025-04-20T18:50:08.518Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload-time = "2025-04-20T18:50:07.196Z" }, +] + +[[package]] +name = "speaklater" +version = "1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/92/5ae1effe0ccb8561c034a0111d53c8788660ddb7ed4992f0da1bb5c525e5/speaklater-1.3.tar.gz", hash = "sha256:59fea336d0eed38c1f0bf3181ee1222d0ef45f3a9dd34ebe65e6bfffdd6a65a9", size = 3582, upload-time = "2012-07-01T18:01:30.306Z" } + +[[package]] +name = "sqlalchemy" +version = "2.0.42" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/03/a0af991e3a43174d6b83fca4fb399745abceddd1171bdabae48ce877ff47/sqlalchemy-2.0.42.tar.gz", hash = "sha256:160bedd8a5c28765bd5be4dec2d881e109e33b34922e50a3b881a7681773ac5f", size = 9749972, upload-time = "2025-07-29T12:48:09.323Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/12/33ff43214c2c6cc87499b402fe419869d2980a08101c991daae31345e901/sqlalchemy-2.0.42-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:172b244753e034d91a826f80a9a70f4cbac690641207f2217f8404c261473efe", size = 2130469, upload-time = "2025-07-29T13:25:15.215Z" }, + { url = "https://files.pythonhosted.org/packages/63/c4/4d2f2c21ddde9a2c7f7b258b202d6af0bac9fc5abfca5de367461c86d766/sqlalchemy-2.0.42-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be28f88abd74af8519a4542185ee80ca914933ca65cdfa99504d82af0e4210df", size = 2120393, upload-time = "2025-07-29T13:25:16.367Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0d/5ff2f2dfbac10e4a9ade1942f8985ffc4bd8f157926b1f8aed553dfe3b88/sqlalchemy-2.0.42-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98b344859d282fde388047f1710860bb23f4098f705491e06b8ab52a48aafea9", size = 3206173, upload-time = "2025-07-29T13:29:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/1f/59/71493fe74bd76a773ae8fa0c50bfc2ccac1cbf7cfa4f9843ad92897e6dcf/sqlalchemy-2.0.42-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97978d223b11f1d161390a96f28c49a13ce48fdd2fed7683167c39bdb1b8aa09", size = 3206910, upload-time = "2025-07-29T13:24:50.58Z" }, + { url = "https://files.pythonhosted.org/packages/a9/51/01b1d85bbb492a36b25df54a070a0f887052e9b190dff71263a09f48576b/sqlalchemy-2.0.42-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e35b9b000c59fcac2867ab3a79fc368a6caca8706741beab3b799d47005b3407", size = 3145479, upload-time = "2025-07-29T13:29:02.3Z" }, + { url = "https://files.pythonhosted.org/packages/fa/78/10834f010e2a3df689f6d1888ea6ea0074ff10184e6a550b8ed7f9189a89/sqlalchemy-2.0.42-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bc7347ad7a7b1c78b94177f2d57263113bb950e62c59b96ed839b131ea4234e1", size = 3169605, upload-time = "2025-07-29T13:24:52.135Z" }, + { url = "https://files.pythonhosted.org/packages/0c/75/e6fdd66d237582c8488dd1dfa90899f6502822fbd866363ab70e8ac4a2ce/sqlalchemy-2.0.42-cp310-cp310-win32.whl", hash = "sha256:739e58879b20a179156b63aa21f05ccacfd3e28e08e9c2b630ff55cd7177c4f1", size = 2098759, upload-time = "2025-07-29T13:23:55.809Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a8/366db192641c2c2d1ea8977e7c77b65a0d16a7858907bb76ea68b9dd37af/sqlalchemy-2.0.42-cp310-cp310-win_amd64.whl", hash = "sha256:1aef304ada61b81f1955196f584b9e72b798ed525a7c0b46e09e98397393297b", size = 2122423, upload-time = "2025-07-29T13:23:56.968Z" }, + { url = "https://files.pythonhosted.org/packages/ea/3c/7bfd65f3c2046e2fb4475b21fa0b9d7995f8c08bfa0948df7a4d2d0de869/sqlalchemy-2.0.42-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c34100c0b7ea31fbc113c124bcf93a53094f8951c7bf39c45f39d327bad6d1e7", size = 2133779, upload-time = "2025-07-29T13:25:18.446Z" }, + { url = "https://files.pythonhosted.org/packages/66/17/19be542fe9dd64a766090e90e789e86bdaa608affda6b3c1e118a25a2509/sqlalchemy-2.0.42-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ad59dbe4d1252448c19d171dfba14c74e7950b46dc49d015722a4a06bfdab2b0", size = 2123843, upload-time = "2025-07-29T13:25:19.749Z" }, + { url = "https://files.pythonhosted.org/packages/14/fc/83e45fc25f0acf1c26962ebff45b4c77e5570abb7c1a425a54b00bcfa9c7/sqlalchemy-2.0.42-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9187498c2149919753a7fd51766ea9c8eecdec7da47c1b955fa8090bc642eaa", size = 3294824, upload-time = "2025-07-29T13:29:03.879Z" }, + { url = "https://files.pythonhosted.org/packages/b9/81/421efc09837104cd1a267d68b470e5b7b6792c2963b8096ca1e060ba0975/sqlalchemy-2.0.42-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f092cf83ebcafba23a247f5e03f99f5436e3ef026d01c8213b5eca48ad6efa9", size = 3294662, upload-time = "2025-07-29T13:24:53.715Z" }, + { url = "https://files.pythonhosted.org/packages/2f/ba/55406e09d32ed5e5f9e8aaec5ef70c4f20b4ae25b9fa9784f4afaa28e7c3/sqlalchemy-2.0.42-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc6afee7e66fdba4f5a68610b487c1f754fccdc53894a9567785932dbb6a265e", size = 3229413, upload-time = "2025-07-29T13:29:05.638Z" }, + { url = "https://files.pythonhosted.org/packages/d4/c4/df596777fce27bde2d1a4a2f5a7ddea997c0c6d4b5246aafba966b421cc0/sqlalchemy-2.0.42-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:260ca1d2e5910f1f1ad3fe0113f8fab28657cee2542cb48c2f342ed90046e8ec", size = 3255563, upload-time = "2025-07-29T13:24:55.17Z" }, + { url = "https://files.pythonhosted.org/packages/16/ed/b9c4a939b314400f43f972c9eb0091da59d8466ef9c51d0fd5b449edc495/sqlalchemy-2.0.42-cp311-cp311-win32.whl", hash = "sha256:2eb539fd83185a85e5fcd6b19214e1c734ab0351d81505b0f987705ba0a1e231", size = 2098513, upload-time = "2025-07-29T13:23:58.946Z" }, + { url = "https://files.pythonhosted.org/packages/91/72/55b0c34e39feb81991aa3c974d85074c356239ac1170dfb81a474b4c23b3/sqlalchemy-2.0.42-cp311-cp311-win_amd64.whl", hash = "sha256:9193fa484bf00dcc1804aecbb4f528f1123c04bad6a08d7710c909750fa76aeb", size = 2123380, upload-time = "2025-07-29T13:24:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/ac31a9821fc70a7376321fb2c70fdd7eadbc06dadf66ee216a22a41d6058/sqlalchemy-2.0.42-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:09637a0872689d3eb71c41e249c6f422e3e18bbd05b4cd258193cfc7a9a50da2", size = 2132203, upload-time = "2025-07-29T13:29:19.291Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ba/fd943172e017f955d7a8b3a94695265b7114efe4854feaa01f057e8f5293/sqlalchemy-2.0.42-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3cb3ec67cc08bea54e06b569398ae21623534a7b1b23c258883a7c696ae10df", size = 2120373, upload-time = "2025-07-29T13:29:21.049Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a2/b5f7d233d063ffadf7e9fff3898b42657ba154a5bec95a96f44cba7f818b/sqlalchemy-2.0.42-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e87e6a5ef6f9d8daeb2ce5918bf5fddecc11cae6a7d7a671fcc4616c47635e01", size = 3317685, upload-time = "2025-07-29T13:26:40.837Z" }, + { url = "https://files.pythonhosted.org/packages/86/00/fcd8daab13a9119d41f3e485a101c29f5d2085bda459154ba354c616bf4e/sqlalchemy-2.0.42-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b718011a9d66c0d2f78e1997755cd965f3414563b31867475e9bc6efdc2281d", size = 3326967, upload-time = "2025-07-29T13:22:31.009Z" }, + { url = "https://files.pythonhosted.org/packages/a3/85/e622a273d648d39d6771157961956991a6d760e323e273d15e9704c30ccc/sqlalchemy-2.0.42-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:16d9b544873fe6486dddbb859501a07d89f77c61d29060bb87d0faf7519b6a4d", size = 3255331, upload-time = "2025-07-29T13:26:42.579Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a0/2c2338b592c7b0a61feffd005378c084b4c01fabaf1ed5f655ab7bd446f0/sqlalchemy-2.0.42-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21bfdf57abf72fa89b97dd74d3187caa3172a78c125f2144764a73970810c4ee", size = 3291791, upload-time = "2025-07-29T13:22:32.454Z" }, + { url = "https://files.pythonhosted.org/packages/41/19/b8a2907972a78285fdce4c880ecaab3c5067eb726882ca6347f7a4bf64f6/sqlalchemy-2.0.42-cp312-cp312-win32.whl", hash = "sha256:78b46555b730a24901ceb4cb901c6b45c9407f8875209ed3c5d6bcd0390a6ed1", size = 2096180, upload-time = "2025-07-29T13:16:08.952Z" }, + { url = "https://files.pythonhosted.org/packages/48/1f/67a78f3dfd08a2ed1c7be820fe7775944f5126080b5027cc859084f8e223/sqlalchemy-2.0.42-cp312-cp312-win_amd64.whl", hash = "sha256:4c94447a016f36c4da80072e6c6964713b0af3c8019e9c4daadf21f61b81ab53", size = 2123533, upload-time = "2025-07-29T13:16:11.705Z" }, + { url = "https://files.pythonhosted.org/packages/e9/7e/25d8c28b86730c9fb0e09156f601d7a96d1c634043bf8ba36513eb78887b/sqlalchemy-2.0.42-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:941804f55c7d507334da38133268e3f6e5b0340d584ba0f277dd884197f4ae8c", size = 2127905, upload-time = "2025-07-29T13:29:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/e5/a1/9d8c93434d1d983880d976400fcb7895a79576bd94dca61c3b7b90b1ed0d/sqlalchemy-2.0.42-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95d3d06a968a760ce2aa6a5889fefcbdd53ca935735e0768e1db046ec08cbf01", size = 2115726, upload-time = "2025-07-29T13:29:23.496Z" }, + { url = "https://files.pythonhosted.org/packages/a2/cc/d33646fcc24c87cc4e30a03556b611a4e7bcfa69a4c935bffb923e3c89f4/sqlalchemy-2.0.42-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cf10396a8a700a0f38ccd220d940be529c8f64435c5d5b29375acab9267a6c9", size = 3246007, upload-time = "2025-07-29T13:26:44.166Z" }, + { url = "https://files.pythonhosted.org/packages/67/08/4e6c533d4c7f5e7c4cbb6fe8a2c4e813202a40f05700d4009a44ec6e236d/sqlalchemy-2.0.42-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9cae6c2b05326d7c2c7c0519f323f90e0fb9e8afa783c6a05bb9ee92a90d0f04", size = 3250919, upload-time = "2025-07-29T13:22:33.74Z" }, + { url = "https://files.pythonhosted.org/packages/5c/82/f680e9a636d217aece1b9a8030d18ad2b59b5e216e0c94e03ad86b344af3/sqlalchemy-2.0.42-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f50f7b20677b23cfb35b6afcd8372b2feb348a38e3033f6447ee0704540be894", size = 3180546, upload-time = "2025-07-29T13:26:45.648Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a2/8c8f6325f153894afa3775584c429cc936353fb1db26eddb60a549d0ff4b/sqlalchemy-2.0.42-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d88a1c0d66d24e229e3938e1ef16ebdbd2bf4ced93af6eff55225f7465cf350", size = 3216683, upload-time = "2025-07-29T13:22:34.977Z" }, + { url = "https://files.pythonhosted.org/packages/39/44/3a451d7fa4482a8ffdf364e803ddc2cfcafc1c4635fb366f169ecc2c3b11/sqlalchemy-2.0.42-cp313-cp313-win32.whl", hash = "sha256:45c842c94c9ad546c72225a0c0d1ae8ef3f7c212484be3d429715a062970e87f", size = 2093990, upload-time = "2025-07-29T13:16:13.036Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9e/9bce34f67aea0251c8ac104f7bdb2229d58fb2e86a4ad8807999c4bee34b/sqlalchemy-2.0.42-cp313-cp313-win_amd64.whl", hash = "sha256:eb9905f7f1e49fd57a7ed6269bc567fcbbdac9feadff20ad6bd7707266a91577", size = 2120473, upload-time = "2025-07-29T13:16:14.502Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/ba2546ab09a6adebc521bf3974440dc1d8c06ed342cceb30ed62a8858835/sqlalchemy-2.0.42-py3-none-any.whl", hash = "sha256:defcdff7e661f0043daa381832af65d616e060ddb54d3fe4476f51df7eaa1835", size = 1922072, upload-time = "2025-07-29T13:09:17.061Z" }, +] + +[[package]] +name = "sqlalchemy-utils" +version = "0.41.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/bf/abfd5474cdd89ddd36dbbde9c6efba16bfa7f5448913eba946fed14729da/SQLAlchemy-Utils-0.41.2.tar.gz", hash = "sha256:bc599c8c3b3319e53ce6c5c3c471120bd325d0071fb6f38a10e924e3d07b9990", size = 138017, upload-time = "2024-03-24T15:17:28.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/f0/dc4757b83ac1ab853cf222df8535ed73973e0c203d983982ba7b8bc60508/SQLAlchemy_Utils-0.41.2-py3-none-any.whl", hash = "sha256:85cf3842da2bf060760f955f8467b87983fb2e30f1764fd0e24a48307dc8ec6e", size = 93083, upload-time = "2024-03-24T15:17:24.533Z" }, +] + +[[package]] +name = "toml" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +] + +[[package]] +name = "tomli-w" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.13.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" }, +] + +[[package]] +name = "trove-classifiers" +version = "2025.5.9.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/04/1cd43f72c241fedcf0d9a18d0783953ee301eac9e5d9db1df0f0f089d9af/trove_classifiers-2025.5.9.12.tar.gz", hash = "sha256:7ca7c8a7a76e2cd314468c677c69d12cc2357711fcab4a60f87994c1589e5cb5", size = 16940, upload-time = "2025-05-09T12:04:48.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/ef/c6deb083748be3bcad6f471b6ae983950c161890bf5ae1b2af80cc56c530/trove_classifiers-2025.5.9.12-py3-none-any.whl", hash = "sha256:e381c05537adac78881c8fa345fd0e9970159f4e4a04fcc42cfd3129cca640ce", size = 14119, upload-time = "2025-05-09T12:04:46.38Z" }, +] + +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20250708" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/95/6bdde7607da2e1e99ec1c1672a759d42f26644bbacf939916e086db34870/types_python_dateutil-2.9.0.20250708.tar.gz", hash = "sha256:ccdbd75dab2d6c9696c350579f34cffe2c281e4c5f27a585b2a2438dd1d5c8ab", size = 15834, upload-time = "2025-07-08T03:14:03.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/52/43e70a8e57fefb172c22a21000b03ebcc15e47e97f5cb8495b9c2832efb4/types_python_dateutil-2.9.0.20250708-py3-none-any.whl", hash = "sha256:4d6d0cc1cc4d24a2dc3816024e502564094497b713f7befda4d5bc7a8e3fd21f", size = 17724, upload-time = "2025-07-08T03:14:02.593Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "userpath" +version = "1.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/b7/30753098208505d7ff9be5b3a32112fb8a4cb3ddfccbbb7ba9973f2e29ff/userpath-1.9.2.tar.gz", hash = "sha256:6c52288dab069257cc831846d15d48133522455d4677ee69a9781f11dbefd815", size = 11140, upload-time = "2024-02-29T21:39:08.742Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl", hash = "sha256:2cbf01a23d655a1ff8fc166dfb78da1b641d1ceabf0fe5f970767d380b14e89d", size = 9065, upload-time = "2024-02-29T21:39:07.551Z" }, +] + +[[package]] +name = "uv" +version = "0.8.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/05/779581d8e5cd8d12dc3e2297280a03293f7b465bb5f53308479e508c5c44/uv-0.8.4.tar.gz", hash = "sha256:2ab21c32a28dbe434c9074f899ed8084955f7b09ac5e7ffac548d3454f77516f", size = 3442716, upload-time = "2025-07-30T17:10:56.404Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/10/4d52b081defca3cfb4a11d6af3af4314fe7f289ba19e40d6cfab778f9257/uv-0.8.4-py3-none-linux_armv6l.whl", hash = "sha256:f9a5da616ca0d2bbe79367db9cf339cbaf1affee5d6b130a3be2779a917c14fa", size = 18077025, upload-time = "2025-07-30T17:10:13.016Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/7847373d214de987e96ef6b820a4ed2fa5e1c392ecc73cd53e94013d6074/uv-0.8.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4d8422b3058998d87fee46d4d1a437e202407cafca8b8ac69e01c6479fbe0271", size = 18143542, upload-time = "2025-07-30T17:10:18.006Z" }, + { url = "https://files.pythonhosted.org/packages/16/39/7d4b68132868c550ae97c3b2c348c55db47a987dff05ab0e5f577bf0e197/uv-0.8.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:edc813645348665a3b4716a7d5e961cf7c8d1d3bfb9d907a4f18cf87c712a430", size = 16860749, upload-time = "2025-07-30T17:10:20.417Z" }, + { url = "https://files.pythonhosted.org/packages/e3/8f/f703e4ba41aae195d4958b701c2ee6cdbbbb8cdccb082845d6abfe834cf9/uv-0.8.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:c2323e915ae562db4ebcdf5e20d3dd37a14959d07cc54939d86ab0dcdbf08f58", size = 17469507, upload-time = "2025-07-30T17:10:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/59/f8/9366ceeb63f9dd6aa11375047762c1033d36521722e748b65a24e435f459/uv-0.8.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:96d7a68c360383d638c283811d57558fbf7b5f769ff4bdbc99ee2a3bf9a6e574", size = 17766700, upload-time = "2025-07-30T17:10:24.903Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e3/190eb0ca91b8a0e5f80f93aeb7924b12be89656066170d6e1244e90c5e80/uv-0.8.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:385dec5a0c0909d5a24af5b02db24b49b025cbed59c6225e4c794ff40069d9aa", size = 18432996, upload-time = "2025-07-30T17:10:27.239Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f6/b5fc5fe6e93e0294cbd8ba228d10b12e46a5e27b143565e868da758e0209/uv-0.8.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b2230310ca303328c9fd351044fb81349f3ccfaa2863f135d37bfcee707adfd1", size = 19842168, upload-time = "2025-07-30T17:10:29.958Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f0/d01779df4ac2ae39bf440c97f53346f1b9eef17cc84a45ed66206e348650/uv-0.8.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d64c66993eb0d9821caea27920175a27cd24df1eba8a340d8b3ae4074fac77", size = 19497445, upload-time = "2025-07-30T17:10:32.064Z" }, + { url = "https://files.pythonhosted.org/packages/80/ca/48c78393cb3a73940e768b74f74c30ca7719de6f83457a125b9cfa0c37e0/uv-0.8.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:624cf5b7bdc5cc0253115fefaad40008205d4acf34b77b294479dfe4eacb9697", size = 18852025, upload-time = "2025-07-30T17:10:34.34Z" }, + { url = "https://files.pythonhosted.org/packages/42/e2/5cf11c85fb48276b49979ea06e92c1e95524e1e4c5bccbd591a334c8de68/uv-0.8.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9cd287982f62419f98ca7182fbbc2fd0fad1a04008b956a88eb85ce1d522611", size = 18806944, upload-time = "2025-07-30T17:10:36.819Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b1/773dcd5ef4947a5bd7c183f1cc8afb9e761488ff1b48b46cb0d95bc5c8cf/uv-0.8.4-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:e6fa3754a2b965dceecfce8c38cacf7cd6b76a2787b9e189cf33acdb64a7472a", size = 17706599, upload-time = "2025-07-30T17:10:38.976Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8f/20dcb6aaa9c9d7e16320b5143b1fdaa5fd1ebc42a99e2d5f4283aafc59f1/uv-0.8.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9f2a7042553e85c66884a6a3c1b88e116bc5fe5e5d1c9b62f025b1de41534734", size = 18564686, upload-time = "2025-07-30T17:10:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/8a/19/9f9df99259d6725fc269d5394606919f32c3e0d21f486277c040cb7c5dad/uv-0.8.4-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:2c80470d7253bd26c5990f4914cfddc68a6bb4da7c7da316a29e99feafe272a1", size = 17722213, upload-time = "2025-07-30T17:10:43.354Z" }, + { url = "https://files.pythonhosted.org/packages/00/f4/358576eea98eb4ba58135690a60f8052dbd8b50173a5c0e93e59c8797c2c/uv-0.8.4-py3-none-musllinux_1_1_i686.whl", hash = "sha256:b90eb86019ff92922dea54b8772074909ce7ab3359b2e8f8f3fe4d0658d3a898", size = 17997363, upload-time = "2025-07-30T17:10:45.631Z" }, + { url = "https://files.pythonhosted.org/packages/51/0f/9e5ff7d73846d8c924a5ef262dee247b453b7b2bd2ba5db1a819c72bd176/uv-0.8.4-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:cad63a02a735ba591679d713376767fc7649ad1e7097a95d0d267a68c2e803fc", size = 18954586, upload-time = "2025-07-30T17:10:47.789Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fa/58c416c634253bdd7ec50baa5d79010f887453425a62e6a23f9668a75305/uv-0.8.4-py3-none-win32.whl", hash = "sha256:b83cd9eeb4c63ab69c6e8d0e26e57b5a9a8b1dca4015f4ddf088ed4a234e7018", size = 17907610, upload-time = "2025-07-30T17:10:49.966Z" }, + { url = "https://files.pythonhosted.org/packages/76/8e/2d6f5bce0f41074122caed1672f90f7ed5df2bd9827c8723c73a657bea7b/uv-0.8.4-py3-none-win_amd64.whl", hash = "sha256:ad056c8f6568d9f495e402753e79a092f28d513e6b5146d1c8dc2bdea668adb1", size = 19704945, upload-time = "2025-07-30T17:10:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/58/de/196e862af4c3b2ff8cb4a7a3ad38ecf0306fa87d03ec9275f16e2f5dc416/uv-0.8.4-py3-none-win_arm64.whl", hash = "sha256:41f3a22550811bf7a0980b3d4dfce09e2c93aec7c42c92313ae3d3d0b97e1054", size = 18316402, upload-time = "2025-07-30T17:10:54.507Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.32.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/96/0834f30fa08dca3738614e6a9d42752b6420ee94e58971d702118f7cfd30/virtualenv-20.32.0.tar.gz", hash = "sha256:886bf75cadfdc964674e6e33eb74d787dff31ca314ceace03ca5810620f4ecf0", size = 6076970, upload-time = "2025-07-21T04:09:50.985Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/c6/f8f28009920a736d0df434b52e9feebfb4d702ba942f15338cb4a83eafc1/virtualenv-20.32.0-py3-none-any.whl", hash = "sha256:2c310aecb62e5aa1b06103ed7c2977b81e042695de2697d01017ff0f1034af56", size = 6057761, upload-time = "2025-07-21T04:09:48.059Z" }, +] + +[[package]] +name = "visitor" +version = "0.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/58/785fcd6de4210049da5fafe62301b197f044f3835393594be368547142b0/visitor-0.1.3.tar.gz", hash = "sha256:2c737903b2b6864ebc6167eef7cf3b997126f1aa94bdf590f90f1436d23e480a", size = 3260, upload-time = "2016-05-18T19:27:53.383Z" } + +[[package]] +name = "waitress" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/cb/04ddb054f45faa306a230769e868c28b8065ea196891f09004ebace5b184/waitress-3.0.2.tar.gz", hash = "sha256:682aaaf2af0c44ada4abfb70ded36393f0e307f4ab9456a215ce0020baefc31f", size = 179901, upload-time = "2024-11-16T20:02:35.195Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/57/a27182528c90ef38d82b636a11f606b0cbb0e17588ed205435f8affe3368/waitress-3.0.2-py3-none-any.whl", hash = "sha256:c56d67fd6e87c2ee598b76abdd4e96cfad1f24cacdea5078d382b1f9d7b5ed2e", size = 56232, upload-time = "2024-11-16T20:02:33.858Z" }, +] + +[[package]] +name = "webgrid" +source = { editable = "." } +dependencies = [ + { name = "blazeutils" }, + { name = "jinja2" }, + { name = "python-dateutil" }, + { name = "sqlalchemy" }, + { name = "werkzeug" }, +] + +[package.optional-dependencies] +i18n = [ + { name = "morphi" }, +] + +[package.dev-dependencies] +audit = [ + { name = "pip-audit" }, +] +dev = [ + { name = "arrow" }, + { name = "click" }, + { name = "flask" }, + { name = "flask-bootstrap" }, + { name = "flask-sqlalchemy" }, + { name = "flask-webtest" }, + { name = "flask-wtf" }, + { name = "hatch" }, + { name = "nox-uv" }, + { name = "openpyxl" }, + { name = "pip-audit" }, + { name = "pre-commit" }, + { name = "pre-commit-uv" }, + { name = "psycopg", extra = ["binary"] }, + { name = "pyquery" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, + { name = "sqlalchemy-utils" }, + { name = "xlsxwriter" }, +] +mssql = [ + { name = "pyodbc" }, +] +nox = [ + { name = "nox-uv" }, +] +pre-commit = [ + { name = "pre-commit" }, + { name = "pre-commit-uv" }, +] +tests = [ + { name = "arrow" }, + { name = "flask" }, + { name = "flask-bootstrap" }, + { name = "flask-sqlalchemy" }, + { name = "flask-webtest" }, + { name = "flask-wtf" }, + { name = "openpyxl" }, + { name = "psycopg", extra = ["binary"] }, + { name = "pyquery" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "sqlalchemy-utils" }, + { name = "xlsxwriter" }, +] + +[package.metadata] +requires-dist = [ + { name = "blazeutils", specifier = ">=0.6.0" }, + { name = "jinja2" }, + { name = "morphi", marker = "extra == 'i18n'" }, + { name = "python-dateutil" }, + { name = "sqlalchemy", specifier = ">=1.4.20" }, + { name = "werkzeug" }, +] +provides-extras = ["i18n"] + +[package.metadata.requires-dev] +audit = [{ name = "pip-audit" }] +dev = [ + { name = "arrow", specifier = ">=1.3.0" }, + { name = "click" }, + { name = "flask", specifier = ">=3.0.3" }, + { name = "flask-bootstrap", specifier = ">=3.3.7.1" }, + { name = "flask-sqlalchemy", specifier = ">=3.1.1" }, + { name = "flask-webtest", specifier = ">=0.1.6" }, + { name = "flask-wtf", specifier = ">=1.2.2" }, + { name = "hatch" }, + { name = "nox-uv" }, + { name = "openpyxl", specifier = ">=3.1.5" }, + { name = "pip-audit" }, + { name = "pre-commit" }, + { name = "pre-commit-uv" }, + { name = "psycopg", extras = ["binary"], specifier = ">=3.2.9" }, + { name = "pyquery", specifier = ">=2.0.1" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, + { name = "sqlalchemy-utils", specifier = ">=0.41.2" }, + { name = "xlsxwriter", specifier = ">=3.2.5" }, +] +mssql = [{ name = "pyodbc" }] +nox = [{ name = "nox-uv" }] +pre-commit = [ + { name = "pre-commit" }, + { name = "pre-commit-uv" }, +] +tests = [ + { name = "arrow", specifier = ">=1.3.0" }, + { name = "flask", specifier = ">=3.0.3" }, + { name = "flask-bootstrap", specifier = ">=3.3.7.1" }, + { name = "flask-sqlalchemy", specifier = ">=3.1.1" }, + { name = "flask-webtest", specifier = ">=0.1.6" }, + { name = "flask-wtf", specifier = ">=1.2.2" }, + { name = "openpyxl", specifier = ">=3.1.5" }, + { name = "psycopg", extras = ["binary"], specifier = ">=3.2.9" }, + { name = "pyquery", specifier = ">=2.0.1" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "sqlalchemy-utils", specifier = ">=0.41.2" }, + { name = "xlsxwriter", specifier = ">=3.2.5" }, +] + +[[package]] +name = "webob" +version = "1.8.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "legacy-cgi", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/0b/1732085540b01f65e4e7999e15864fe14cd18b12a95731a43fd6fd11b26a/webob-1.8.9.tar.gz", hash = "sha256:ad6078e2edb6766d1334ec3dee072ac6a7f95b1e32ce10def8ff7f0f02d56589", size = 279775, upload-time = "2024-10-24T03:19:20.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/bd/c336448be43d40be28e71f2e0f3caf7ccb28e2755c58f4c02c065bfe3e8e/WebOb-1.8.9-py2.py3-none-any.whl", hash = "sha256:45e34c58ed0c7e2ecd238ffd34432487ff13d9ad459ddfd77895e67abba7c1f9", size = 115364, upload-time = "2024-10-24T03:19:18.642Z" }, +] + +[[package]] +name = "webtest" +version = "3.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "waitress" }, + { name = "webob" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/3d/1bead94691f496ea85de7cf0d317e20318fa8813010a346cc1034f6d8fbd/webtest-3.0.6.tar.gz", hash = "sha256:4256fd5242448f56c575bcb9afe275e305a6f0723c4b01438dbdd4dd5344944b", size = 80151, upload-time = "2025-06-04T19:59:01.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/72/22f292e65be0d68f19bb5cc439119453ba60ea1e7fec2a38217fa3d0de28/webtest-3.0.6-py3-none-any.whl", hash = "sha256:0c4a5a3dcf745a78c7905b803d6a520a2cf241c1f00ccdf4351f52916d555543", size = 32368, upload-time = "2025-06-04T19:58:59.232Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925, upload-time = "2024-11-08T15:52:18.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" }, +] + +[[package]] +name = "wrapt" +version = "1.17.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531, upload-time = "2025-01-14T10:35:45.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/d1/1daec934997e8b160040c78d7b31789f19b122110a75eca3d4e8da0049e1/wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984", size = 53307, upload-time = "2025-01-14T10:33:13.616Z" }, + { url = "https://files.pythonhosted.org/packages/1b/7b/13369d42651b809389c1a7153baa01d9700430576c81a2f5c5e460df0ed9/wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22", size = 38486, upload-time = "2025-01-14T10:33:15.947Z" }, + { url = "https://files.pythonhosted.org/packages/62/bf/e0105016f907c30b4bd9e377867c48c34dc9c6c0c104556c9c9126bd89ed/wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7", size = 38777, upload-time = "2025-01-14T10:33:17.462Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/0f6e0679845cbf8b165e027d43402a55494779295c4b08414097b258ac87/wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c", size = 83314, upload-time = "2025-01-14T10:33:21.282Z" }, + { url = "https://files.pythonhosted.org/packages/0f/77/0576d841bf84af8579124a93d216f55d6f74374e4445264cb378a6ed33eb/wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72", size = 74947, upload-time = "2025-01-14T10:33:24.414Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/00759565518f268ed707dcc40f7eeec38637d46b098a1f5143bff488fe97/wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061", size = 82778, upload-time = "2025-01-14T10:33:26.152Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5a/7cffd26b1c607b0b0c8a9ca9d75757ad7620c9c0a9b4a25d3f8a1480fafc/wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2", size = 81716, upload-time = "2025-01-14T10:33:27.372Z" }, + { url = "https://files.pythonhosted.org/packages/7e/09/dccf68fa98e862df7e6a60a61d43d644b7d095a5fc36dbb591bbd4a1c7b2/wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c", size = 74548, upload-time = "2025-01-14T10:33:28.52Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/067021fa3c8814952c5e228d916963c1115b983e21393289de15128e867e/wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62", size = 81334, upload-time = "2025-01-14T10:33:29.643Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0d/9d4b5219ae4393f718699ca1c05f5ebc0c40d076f7e65fd48f5f693294fb/wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563", size = 36427, upload-time = "2025-01-14T10:33:30.832Z" }, + { url = "https://files.pythonhosted.org/packages/72/6a/c5a83e8f61aec1e1aeef939807602fb880e5872371e95df2137142f5c58e/wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f", size = 38774, upload-time = "2025-01-14T10:33:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/a2aab2cbc7a665efab072344a8949a71081eed1d2f451f7f7d2b966594a2/wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58", size = 53308, upload-time = "2025-01-14T10:33:33.992Z" }, + { url = "https://files.pythonhosted.org/packages/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488, upload-time = "2025-01-14T10:33:35.264Z" }, + { url = "https://files.pythonhosted.org/packages/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776, upload-time = "2025-01-14T10:33:38.28Z" }, + { url = "https://files.pythonhosted.org/packages/ca/74/336c918d2915a4943501c77566db41d1bd6e9f4dbc317f356b9a244dfe83/wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", size = 83776, upload-time = "2025-01-14T10:33:40.678Z" }, + { url = "https://files.pythonhosted.org/packages/09/99/c0c844a5ccde0fe5761d4305485297f91d67cf2a1a824c5f282e661ec7ff/wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", size = 75420, upload-time = "2025-01-14T10:33:41.868Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b0/9fc566b0fe08b282c850063591a756057c3247b2362b9286429ec5bf1721/wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", size = 83199, upload-time = "2025-01-14T10:33:43.598Z" }, + { url = "https://files.pythonhosted.org/packages/9d/4b/71996e62d543b0a0bd95dda485219856def3347e3e9380cc0d6cf10cfb2f/wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", size = 82307, upload-time = "2025-01-14T10:33:48.499Z" }, + { url = "https://files.pythonhosted.org/packages/39/35/0282c0d8789c0dc9bcc738911776c762a701f95cfe113fb8f0b40e45c2b9/wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", size = 75025, upload-time = "2025-01-14T10:33:51.191Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6d/90c9fd2c3c6fee181feecb620d95105370198b6b98a0770cba090441a828/wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", size = 81879, upload-time = "2025-01-14T10:33:52.328Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fa/9fb6e594f2ce03ef03eddbdb5f4f90acb1452221a5351116c7c4708ac865/wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", size = 36419, upload-time = "2025-01-14T10:33:53.551Z" }, + { url = "https://files.pythonhosted.org/packages/47/f8/fb1773491a253cbc123c5d5dc15c86041f746ed30416535f2a8df1f4a392/wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", size = 38773, upload-time = "2025-01-14T10:33:56.323Z" }, + { url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799, upload-time = "2025-01-14T10:33:57.4Z" }, + { url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821, upload-time = "2025-01-14T10:33:59.334Z" }, + { url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919, upload-time = "2025-01-14T10:34:04.093Z" }, + { url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721, upload-time = "2025-01-14T10:34:07.163Z" }, + { url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899, upload-time = "2025-01-14T10:34:09.82Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222, upload-time = "2025-01-14T10:34:11.258Z" }, + { url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707, upload-time = "2025-01-14T10:34:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685, upload-time = "2025-01-14T10:34:15.043Z" }, + { url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567, upload-time = "2025-01-14T10:34:16.563Z" }, + { url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672, upload-time = "2025-01-14T10:34:17.727Z" }, + { url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865, upload-time = "2025-01-14T10:34:19.577Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800, upload-time = "2025-01-14T10:34:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824, upload-time = "2025-01-14T10:34:22.999Z" }, + { url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920, upload-time = "2025-01-14T10:34:25.386Z" }, + { url = "https://files.pythonhosted.org/packages/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690, upload-time = "2025-01-14T10:34:28.058Z" }, + { url = "https://files.pythonhosted.org/packages/71/d7/cfcf842291267bf455b3e266c0c29dcb675b5540ee8b50ba1699abf3af45/wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", size = 80861, upload-time = "2025-01-14T10:34:29.167Z" }, + { url = "https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", size = 89174, upload-time = "2025-01-14T10:34:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721, upload-time = "2025-01-14T10:34:32.91Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763, upload-time = "2025-01-14T10:34:34.903Z" }, + { url = "https://files.pythonhosted.org/packages/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585, upload-time = "2025-01-14T10:34:36.13Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676, upload-time = "2025-01-14T10:34:37.962Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871, upload-time = "2025-01-14T10:34:39.13Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312, upload-time = "2025-01-14T10:34:40.604Z" }, + { url = "https://files.pythonhosted.org/packages/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062, upload-time = "2025-01-14T10:34:45.011Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155, upload-time = "2025-01-14T10:34:47.25Z" }, + { url = "https://files.pythonhosted.org/packages/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471, upload-time = "2025-01-14T10:34:50.934Z" }, + { url = "https://files.pythonhosted.org/packages/32/98/4ed894cf012b6d6aae5f5cc974006bdeb92f0241775addad3f8cd6ab71c8/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", size = 101208, upload-time = "2025-01-14T10:34:52.297Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fd/0c30f2301ca94e655e5e057012e83284ce8c545df7661a78d8bfca2fac7a/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", size = 109339, upload-time = "2025-01-14T10:34:53.489Z" }, + { url = "https://files.pythonhosted.org/packages/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232, upload-time = "2025-01-14T10:34:55.327Z" }, + { url = "https://files.pythonhosted.org/packages/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476, upload-time = "2025-01-14T10:34:58.055Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377, upload-time = "2025-01-14T10:34:59.3Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986, upload-time = "2025-01-14T10:35:00.498Z" }, + { url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750, upload-time = "2025-01-14T10:35:03.378Z" }, + { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594, upload-time = "2025-01-14T10:35:44.018Z" }, +] + +[[package]] +name = "wtforms" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/e4/633d080897e769ed5712dcfad626e55dbd6cf45db0ff4d9884315c6a82da/wtforms-3.2.1.tar.gz", hash = "sha256:df3e6b70f3192e92623128123ec8dca3067df9cfadd43d59681e210cfb8d4682", size = 137801, upload-time = "2024-10-21T11:34:00.108Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/c9/2088fb5645cd289c99ebe0d4cdcc723922a1d8e1beaefb0f6f76dff9b21c/wtforms-3.2.1-py3-none-any.whl", hash = "sha256:583bad77ba1dd7286463f21e11aa3043ca4869d03575921d1a1698d0715e0fd4", size = 152454, upload-time = "2024-10-21T11:33:58.44Z" }, +] + +[[package]] +name = "xlsxwriter" +version = "3.2.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/47/7704bac42ac6fe1710ae099b70e6a1e68ed173ef14792b647808c357da43/xlsxwriter-3.2.5.tar.gz", hash = "sha256:7e88469d607cdc920151c0ab3ce9cf1a83992d4b7bc730c5ffdd1a12115a7dbe", size = 213306, upload-time = "2025-06-17T08:59:14.619Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/34/a22e6664211f0c8879521328000bdcae9bf6dbafa94a923e531f6d5b3f73/xlsxwriter-3.2.5-py3-none-any.whl", hash = "sha256:4f4824234e1eaf9d95df9a8fe974585ff91d0f5e3d3f12ace5b71e443c1c6abd", size = 172347, upload-time = "2025-06-17T08:59:13.453Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] + +[[package]] +name = "zstandard" +version = "0.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation == 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/2ac0287b442160a89d726b17a9184a4c615bb5237db763791a7fd16d9df1/zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09", size = 681701, upload-time = "2024-07-15T00:18:06.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/55/bd0487e86679db1823fc9ee0d8c9c78ae2413d34c0b461193b5f4c31d22f/zstandard-0.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bf0a05b6059c0528477fba9054d09179beb63744355cab9f38059548fedd46a9", size = 788701, upload-time = "2024-07-15T00:13:27.351Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8a/ccb516b684f3ad987dfee27570d635822e3038645b1a950c5e8022df1145/zstandard-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fc9ca1c9718cb3b06634c7c8dec57d24e9438b2aa9a0f02b8bb36bf478538880", size = 633678, upload-time = "2024-07-15T00:13:30.24Z" }, + { url = "https://files.pythonhosted.org/packages/12/89/75e633d0611c028e0d9af6df199423bf43f54bea5007e6718ab7132e234c/zstandard-0.23.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77da4c6bfa20dd5ea25cbf12c76f181a8e8cd7ea231c673828d0386b1740b8dc", size = 4941098, upload-time = "2024-07-15T00:13:32.526Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7a/bd7f6a21802de358b63f1ee636ab823711c25ce043a3e9f043b4fcb5ba32/zstandard-0.23.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2170c7e0367dde86a2647ed5b6f57394ea7f53545746104c6b09fc1f4223573", size = 5308798, upload-time = "2024-07-15T00:13:34.925Z" }, + { url = "https://files.pythonhosted.org/packages/79/3b/775f851a4a65013e88ca559c8ae42ac1352db6fcd96b028d0df4d7d1d7b4/zstandard-0.23.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c16842b846a8d2a145223f520b7e18b57c8f476924bda92aeee3a88d11cfc391", size = 5341840, upload-time = "2024-07-15T00:13:37.376Z" }, + { url = "https://files.pythonhosted.org/packages/09/4f/0cc49570141dd72d4d95dd6fcf09328d1b702c47a6ec12fbed3b8aed18a5/zstandard-0.23.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:157e89ceb4054029a289fb504c98c6a9fe8010f1680de0201b3eb5dc20aa6d9e", size = 5440337, upload-time = "2024-07-15T00:13:39.772Z" }, + { url = "https://files.pythonhosted.org/packages/e7/7c/aaa7cd27148bae2dc095191529c0570d16058c54c4597a7d118de4b21676/zstandard-0.23.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:203d236f4c94cd8379d1ea61db2fce20730b4c38d7f1c34506a31b34edc87bdd", size = 4861182, upload-time = "2024-07-15T00:13:42.495Z" }, + { url = "https://files.pythonhosted.org/packages/ac/eb/4b58b5c071d177f7dc027129d20bd2a44161faca6592a67f8fcb0b88b3ae/zstandard-0.23.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dc5d1a49d3f8262be192589a4b72f0d03b72dcf46c51ad5852a4fdc67be7b9e4", size = 4932936, upload-time = "2024-07-15T00:13:44.234Z" }, + { url = "https://files.pythonhosted.org/packages/44/f9/21a5fb9bb7c9a274b05ad700a82ad22ce82f7ef0f485980a1e98ed6e8c5f/zstandard-0.23.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:752bf8a74412b9892f4e5b58f2f890a039f57037f52c89a740757ebd807f33ea", size = 5464705, upload-time = "2024-07-15T00:13:46.822Z" }, + { url = "https://files.pythonhosted.org/packages/49/74/b7b3e61db3f88632776b78b1db597af3f44c91ce17d533e14a25ce6a2816/zstandard-0.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80080816b4f52a9d886e67f1f96912891074903238fe54f2de8b786f86baded2", size = 4857882, upload-time = "2024-07-15T00:13:49.297Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7f/d8eb1cb123d8e4c541d4465167080bec88481ab54cd0b31eb4013ba04b95/zstandard-0.23.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:84433dddea68571a6d6bd4fbf8ff398236031149116a7fff6f777ff95cad3df9", size = 4697672, upload-time = "2024-07-15T00:13:51.447Z" }, + { url = "https://files.pythonhosted.org/packages/5e/05/f7dccdf3d121309b60342da454d3e706453a31073e2c4dac8e1581861e44/zstandard-0.23.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ab19a2d91963ed9e42b4e8d77cd847ae8381576585bad79dbd0a8837a9f6620a", size = 5206043, upload-time = "2024-07-15T00:13:53.587Z" }, + { url = "https://files.pythonhosted.org/packages/86/9d/3677a02e172dccd8dd3a941307621c0cbd7691d77cb435ac3c75ab6a3105/zstandard-0.23.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:59556bf80a7094d0cfb9f5e50bb2db27fefb75d5138bb16fb052b61b0e0eeeb0", size = 5667390, upload-time = "2024-07-15T00:13:56.137Z" }, + { url = "https://files.pythonhosted.org/packages/41/7e/0012a02458e74a7ba122cd9cafe491facc602c9a17f590367da369929498/zstandard-0.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:27d3ef2252d2e62476389ca8f9b0cf2bbafb082a3b6bfe9d90cbcbb5529ecf7c", size = 5198901, upload-time = "2024-07-15T00:13:58.584Z" }, + { url = "https://files.pythonhosted.org/packages/65/3a/8f715b97bd7bcfc7342d8adcd99a026cb2fb550e44866a3b6c348e1b0f02/zstandard-0.23.0-cp310-cp310-win32.whl", hash = "sha256:5d41d5e025f1e0bccae4928981e71b2334c60f580bdc8345f824e7c0a4c2a813", size = 430596, upload-time = "2024-07-15T00:14:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/19/b7/b2b9eca5e5a01111e4fe8a8ffb56bdcdf56b12448a24effe6cfe4a252034/zstandard-0.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:519fbf169dfac1222a76ba8861ef4ac7f0530c35dd79ba5727014613f91613d4", size = 495498, upload-time = "2024-07-15T00:14:02.741Z" }, + { url = "https://files.pythonhosted.org/packages/9e/40/f67e7d2c25a0e2dc1744dd781110b0b60306657f8696cafb7ad7579469bd/zstandard-0.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:34895a41273ad33347b2fc70e1bff4240556de3c46c6ea430a7ed91f9042aa4e", size = 788699, upload-time = "2024-07-15T00:14:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/e8/46/66d5b55f4d737dd6ab75851b224abf0afe5774976fe511a54d2eb9063a41/zstandard-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:77ea385f7dd5b5676d7fd943292ffa18fbf5c72ba98f7d09fc1fb9e819b34c23", size = 633681, upload-time = "2024-07-15T00:14:13.99Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/677e65c095d8e12b66b8f862b069bcf1f1d781b9c9c6f12eb55000d57583/zstandard-0.23.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:983b6efd649723474f29ed42e1467f90a35a74793437d0bc64a5bf482bedfa0a", size = 4944328, upload-time = "2024-07-15T00:14:16.588Z" }, + { url = "https://files.pythonhosted.org/packages/59/cc/e76acb4c42afa05a9d20827116d1f9287e9c32b7ad58cc3af0721ce2b481/zstandard-0.23.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80a539906390591dd39ebb8d773771dc4db82ace6372c4d41e2d293f8e32b8db", size = 5311955, upload-time = "2024-07-15T00:14:19.389Z" }, + { url = "https://files.pythonhosted.org/packages/78/e4/644b8075f18fc7f632130c32e8f36f6dc1b93065bf2dd87f03223b187f26/zstandard-0.23.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:445e4cb5048b04e90ce96a79b4b63140e3f4ab5f662321975679b5f6360b90e2", size = 5344944, upload-time = "2024-07-15T00:14:22.173Z" }, + { url = "https://files.pythonhosted.org/packages/76/3f/dbafccf19cfeca25bbabf6f2dd81796b7218f768ec400f043edc767015a6/zstandard-0.23.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd30d9c67d13d891f2360b2a120186729c111238ac63b43dbd37a5a40670b8ca", size = 5442927, upload-time = "2024-07-15T00:14:24.825Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/d24a01a19b6733b9f218e94d1a87c477d523237e07f94899e1c10f6fd06c/zstandard-0.23.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d20fd853fbb5807c8e84c136c278827b6167ded66c72ec6f9a14b863d809211c", size = 4864910, upload-time = "2024-07-15T00:14:26.982Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a9/cf8f78ead4597264f7618d0875be01f9bc23c9d1d11afb6d225b867cb423/zstandard-0.23.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed1708dbf4d2e3a1c5c69110ba2b4eb6678262028afd6c6fbcc5a8dac9cda68e", size = 4935544, upload-time = "2024-07-15T00:14:29.582Z" }, + { url = "https://files.pythonhosted.org/packages/2c/96/8af1e3731b67965fb995a940c04a2c20997a7b3b14826b9d1301cf160879/zstandard-0.23.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:be9b5b8659dff1f913039c2feee1aca499cfbc19e98fa12bc85e037c17ec6ca5", size = 5467094, upload-time = "2024-07-15T00:14:40.126Z" }, + { url = "https://files.pythonhosted.org/packages/ff/57/43ea9df642c636cb79f88a13ab07d92d88d3bfe3e550b55a25a07a26d878/zstandard-0.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:65308f4b4890aa12d9b6ad9f2844b7ee42c7f7a4fd3390425b242ffc57498f48", size = 4860440, upload-time = "2024-07-15T00:14:42.786Z" }, + { url = "https://files.pythonhosted.org/packages/46/37/edb78f33c7f44f806525f27baa300341918fd4c4af9472fbc2c3094be2e8/zstandard-0.23.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98da17ce9cbf3bfe4617e836d561e433f871129e3a7ac16d6ef4c680f13a839c", size = 4700091, upload-time = "2024-07-15T00:14:45.184Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f1/454ac3962671a754f3cb49242472df5c2cced4eb959ae203a377b45b1a3c/zstandard-0.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8ed7d27cb56b3e058d3cf684d7200703bcae623e1dcc06ed1e18ecda39fee003", size = 5208682, upload-time = "2024-07-15T00:14:47.407Z" }, + { url = "https://files.pythonhosted.org/packages/85/b2/1734b0fff1634390b1b887202d557d2dd542de84a4c155c258cf75da4773/zstandard-0.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:b69bb4f51daf461b15e7b3db033160937d3ff88303a7bc808c67bbc1eaf98c78", size = 5669707, upload-time = "2024-07-15T00:15:03.529Z" }, + { url = "https://files.pythonhosted.org/packages/52/5a/87d6971f0997c4b9b09c495bf92189fb63de86a83cadc4977dc19735f652/zstandard-0.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:034b88913ecc1b097f528e42b539453fa82c3557e414b3de9d5632c80439a473", size = 5201792, upload-time = "2024-07-15T00:15:28.372Z" }, + { url = "https://files.pythonhosted.org/packages/79/02/6f6a42cc84459d399bd1a4e1adfc78d4dfe45e56d05b072008d10040e13b/zstandard-0.23.0-cp311-cp311-win32.whl", hash = "sha256:f2d4380bf5f62daabd7b751ea2339c1a21d1c9463f1feb7fc2bdcea2c29c3160", size = 430586, upload-time = "2024-07-15T00:15:32.26Z" }, + { url = "https://files.pythonhosted.org/packages/be/a2/4272175d47c623ff78196f3c10e9dc7045c1b9caf3735bf041e65271eca4/zstandard-0.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:62136da96a973bd2557f06ddd4e8e807f9e13cbb0bfb9cc06cfe6d98ea90dfe0", size = 495420, upload-time = "2024-07-15T00:15:34.004Z" }, + { url = "https://files.pythonhosted.org/packages/7b/83/f23338c963bd9de687d47bf32efe9fd30164e722ba27fb59df33e6b1719b/zstandard-0.23.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b4567955a6bc1b20e9c31612e615af6b53733491aeaa19a6b3b37f3b65477094", size = 788713, upload-time = "2024-07-15T00:15:35.815Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b3/1a028f6750fd9227ee0b937a278a434ab7f7fdc3066c3173f64366fe2466/zstandard-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e172f57cd78c20f13a3415cc8dfe24bf388614324d25539146594c16d78fcc8", size = 633459, upload-time = "2024-07-15T00:15:37.995Z" }, + { url = "https://files.pythonhosted.org/packages/26/af/36d89aae0c1f95a0a98e50711bc5d92c144939efc1f81a2fcd3e78d7f4c1/zstandard-0.23.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0e166f698c5a3e914947388c162be2583e0c638a4703fc6a543e23a88dea3c1", size = 4945707, upload-time = "2024-07-15T00:15:39.872Z" }, + { url = "https://files.pythonhosted.org/packages/cd/2e/2051f5c772f4dfc0aae3741d5fc72c3dcfe3aaeb461cc231668a4db1ce14/zstandard-0.23.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a289832e520c6bd4dcaad68e944b86da3bad0d339ef7989fb7e88f92e96072", size = 5306545, upload-time = "2024-07-15T00:15:41.75Z" }, + { url = "https://files.pythonhosted.org/packages/0a/9e/a11c97b087f89cab030fa71206963090d2fecd8eb83e67bb8f3ffb84c024/zstandard-0.23.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d50d31bfedd53a928fed6707b15a8dbeef011bb6366297cc435accc888b27c20", size = 5337533, upload-time = "2024-07-15T00:15:44.114Z" }, + { url = "https://files.pythonhosted.org/packages/fc/79/edeb217c57fe1bf16d890aa91a1c2c96b28c07b46afed54a5dcf310c3f6f/zstandard-0.23.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72c68dda124a1a138340fb62fa21b9bf4848437d9ca60bd35db36f2d3345f373", size = 5436510, upload-time = "2024-07-15T00:15:46.509Z" }, + { url = "https://files.pythonhosted.org/packages/81/4f/c21383d97cb7a422ddf1ae824b53ce4b51063d0eeb2afa757eb40804a8ef/zstandard-0.23.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53dd9d5e3d29f95acd5de6802e909ada8d8d8cfa37a3ac64836f3bc4bc5512db", size = 4859973, upload-time = "2024-07-15T00:15:49.939Z" }, + { url = "https://files.pythonhosted.org/packages/ab/15/08d22e87753304405ccac8be2493a495f529edd81d39a0870621462276ef/zstandard-0.23.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6a41c120c3dbc0d81a8e8adc73312d668cd34acd7725f036992b1b72d22c1772", size = 4936968, upload-time = "2024-07-15T00:15:52.025Z" }, + { url = "https://files.pythonhosted.org/packages/eb/fa/f3670a597949fe7dcf38119a39f7da49a8a84a6f0b1a2e46b2f71a0ab83f/zstandard-0.23.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:40b33d93c6eddf02d2c19f5773196068d875c41ca25730e8288e9b672897c105", size = 5467179, upload-time = "2024-07-15T00:15:54.971Z" }, + { url = "https://files.pythonhosted.org/packages/4e/a9/dad2ab22020211e380adc477a1dbf9f109b1f8d94c614944843e20dc2a99/zstandard-0.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9206649ec587e6b02bd124fb7799b86cddec350f6f6c14bc82a2b70183e708ba", size = 4848577, upload-time = "2024-07-15T00:15:57.634Z" }, + { url = "https://files.pythonhosted.org/packages/08/03/dd28b4484b0770f1e23478413e01bee476ae8227bbc81561f9c329e12564/zstandard-0.23.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76e79bc28a65f467e0409098fa2c4376931fd3207fbeb6b956c7c476d53746dd", size = 4693899, upload-time = "2024-07-15T00:16:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/2b/64/3da7497eb635d025841e958bcd66a86117ae320c3b14b0ae86e9e8627518/zstandard-0.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:66b689c107857eceabf2cf3d3fc699c3c0fe8ccd18df2219d978c0283e4c508a", size = 5199964, upload-time = "2024-07-15T00:16:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/43/a4/d82decbab158a0e8a6ebb7fc98bc4d903266bce85b6e9aaedea1d288338c/zstandard-0.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9c236e635582742fee16603042553d276cca506e824fa2e6489db04039521e90", size = 5655398, upload-time = "2024-07-15T00:16:06.694Z" }, + { url = "https://files.pythonhosted.org/packages/f2/61/ac78a1263bc83a5cf29e7458b77a568eda5a8f81980691bbc6eb6a0d45cc/zstandard-0.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8fffdbd9d1408006baaf02f1068d7dd1f016c6bcb7538682622c556e7b68e35", size = 5191313, upload-time = "2024-07-15T00:16:09.758Z" }, + { url = "https://files.pythonhosted.org/packages/e7/54/967c478314e16af5baf849b6ee9d6ea724ae5b100eb506011f045d3d4e16/zstandard-0.23.0-cp312-cp312-win32.whl", hash = "sha256:dc1d33abb8a0d754ea4763bad944fd965d3d95b5baef6b121c0c9013eaf1907d", size = 430877, upload-time = "2024-07-15T00:16:11.758Z" }, + { url = "https://files.pythonhosted.org/packages/75/37/872d74bd7739639c4553bf94c84af7d54d8211b626b352bc57f0fd8d1e3f/zstandard-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:64585e1dba664dc67c7cdabd56c1e5685233fbb1fc1966cfba2a340ec0dfff7b", size = 495595, upload-time = "2024-07-15T00:16:13.731Z" }, + { url = "https://files.pythonhosted.org/packages/80/f1/8386f3f7c10261fe85fbc2c012fdb3d4db793b921c9abcc995d8da1b7a80/zstandard-0.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:576856e8594e6649aee06ddbfc738fec6a834f7c85bf7cadd1c53d4a58186ef9", size = 788975, upload-time = "2024-07-15T00:16:16.005Z" }, + { url = "https://files.pythonhosted.org/packages/16/e8/cbf01077550b3e5dc86089035ff8f6fbbb312bc0983757c2d1117ebba242/zstandard-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38302b78a850ff82656beaddeb0bb989a0322a8bbb1bf1ab10c17506681d772a", size = 633448, upload-time = "2024-07-15T00:16:17.897Z" }, + { url = "https://files.pythonhosted.org/packages/06/27/4a1b4c267c29a464a161aeb2589aff212b4db653a1d96bffe3598f3f0d22/zstandard-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2240ddc86b74966c34554c49d00eaafa8200a18d3a5b6ffbf7da63b11d74ee2", size = 4945269, upload-time = "2024-07-15T00:16:20.136Z" }, + { url = "https://files.pythonhosted.org/packages/7c/64/d99261cc57afd9ae65b707e38045ed8269fbdae73544fd2e4a4d50d0ed83/zstandard-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ef230a8fd217a2015bc91b74f6b3b7d6522ba48be29ad4ea0ca3a3775bf7dd5", size = 5306228, upload-time = "2024-07-15T00:16:23.398Z" }, + { url = "https://files.pythonhosted.org/packages/7a/cf/27b74c6f22541f0263016a0fd6369b1b7818941de639215c84e4e94b2a1c/zstandard-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:774d45b1fac1461f48698a9d4b5fa19a69d47ece02fa469825b442263f04021f", size = 5336891, upload-time = "2024-07-15T00:16:26.391Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/89ac62eac46b69948bf35fcd90d37103f38722968e2981f752d69081ec4d/zstandard-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f77fa49079891a4aab203d0b1744acc85577ed16d767b52fc089d83faf8d8ed", size = 5436310, upload-time = "2024-07-15T00:16:29.018Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a8/5ca5328ee568a873f5118d5b5f70d1f36c6387716efe2e369010289a5738/zstandard-0.23.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac184f87ff521f4840e6ea0b10c0ec90c6b1dcd0bad2f1e4a9a1b4fa177982ea", size = 4859912, upload-time = "2024-07-15T00:16:31.871Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ca/3781059c95fd0868658b1cf0440edd832b942f84ae60685d0cfdb808bca1/zstandard-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c363b53e257246a954ebc7c488304b5592b9c53fbe74d03bc1c64dda153fb847", size = 4936946, upload-time = "2024-07-15T00:16:34.593Z" }, + { url = "https://files.pythonhosted.org/packages/ce/11/41a58986f809532742c2b832c53b74ba0e0a5dae7e8ab4642bf5876f35de/zstandard-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e7792606d606c8df5277c32ccb58f29b9b8603bf83b48639b7aedf6df4fe8171", size = 5466994, upload-time = "2024-07-15T00:16:36.887Z" }, + { url = "https://files.pythonhosted.org/packages/83/e3/97d84fe95edd38d7053af05159465d298c8b20cebe9ccb3d26783faa9094/zstandard-0.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0817825b900fcd43ac5d05b8b3079937073d2b1ff9cf89427590718b70dd840", size = 4848681, upload-time = "2024-07-15T00:16:39.709Z" }, + { url = "https://files.pythonhosted.org/packages/6e/99/cb1e63e931de15c88af26085e3f2d9af9ce53ccafac73b6e48418fd5a6e6/zstandard-0.23.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9da6bc32faac9a293ddfdcb9108d4b20416219461e4ec64dfea8383cac186690", size = 4694239, upload-time = "2024-07-15T00:16:41.83Z" }, + { url = "https://files.pythonhosted.org/packages/ab/50/b1e703016eebbc6501fc92f34db7b1c68e54e567ef39e6e59cf5fb6f2ec0/zstandard-0.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fd7699e8fd9969f455ef2926221e0233f81a2542921471382e77a9e2f2b57f4b", size = 5200149, upload-time = "2024-07-15T00:16:44.287Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e0/932388630aaba70197c78bdb10cce2c91fae01a7e553b76ce85471aec690/zstandard-0.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d477ed829077cd945b01fc3115edd132c47e6540ddcd96ca169facff28173057", size = 5655392, upload-time = "2024-07-15T00:16:46.423Z" }, + { url = "https://files.pythonhosted.org/packages/02/90/2633473864f67a15526324b007a9f96c96f56d5f32ef2a56cc12f9548723/zstandard-0.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ce8b52c5987b3e34d5674b0ab529a4602b632ebab0a93b07bfb4dfc8f8a33", size = 5191299, upload-time = "2024-07-15T00:16:49.053Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4c/315ca5c32da7e2dc3455f3b2caee5c8c2246074a61aac6ec3378a97b7136/zstandard-0.23.0-cp313-cp313-win32.whl", hash = "sha256:a9b07268d0c3ca5c170a385a0ab9fb7fdd9f5fd866be004c4ea39e44edce47dd", size = 430862, upload-time = "2024-07-15T00:16:51.003Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bf/c6aaba098e2d04781e8f4f7c0ba3c7aa73d00e4c436bcc0cf059a66691d1/zstandard-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:f3513916e8c645d0610815c257cbfd3242adfd5c4cfa78be514e5a3ebb42a41b", size = 495578, upload-time = "2024-07-15T00:16:53.135Z" }, +] From 1407596b2dbba6433513cf3cd278afb304a54f87 Mon Sep 17 00:00:00 2001 From: Randy Syring Date: Sat, 2 Aug 2025 19:40:43 -0400 Subject: [PATCH 10/33] Ruff: bulk safe fixes and formatting --- docs/source/conf.py | 11 +- noxfile.py | 3 +- src/webgrid/__init__.py | 453 +++++++---- src/webgrid/blazeweb.py | 13 +- src/webgrid/extensions.py | 41 +- src/webgrid/filters.py | 884 +++++++++++++-------- src/webgrid/flask.py | 78 +- src/webgrid/renderers.py | 288 +++---- src/webgrid/testing.py | 160 ++-- src/webgrid/types.py | 54 +- src/webgrid/utils.py | 10 +- src/webgrid/validators.py | 20 +- src/webgrid_blazeweb_ta/config/settings.py | 1 + src/webgrid_blazeweb_ta/model/orm.py | 6 +- src/webgrid_blazeweb_ta/tasks/init_db.py | 13 +- src/webgrid_blazeweb_ta/tests/grids.py | 32 +- src/webgrid_blazeweb_ta/views.py | 2 +- src/webgrid_ta/app.py | 8 +- src/webgrid_ta/grids.py | 44 +- src/webgrid_ta/helpers.py | 26 +- src/webgrid_ta/manage.py | 9 +- src/webgrid_ta/model/__init__.py | 12 +- src/webgrid_ta/model/entities.py | 7 +- src/webgrid_ta/model/helpers.py | 131 +-- src/webgrid_ta/views.py | 2 +- src/webgrid_tests/conftest.py | 56 +- src/webgrid_tests/helpers.py | 12 +- src/webgrid_tests/test_api.py | 486 +++++------ src/webgrid_tests/test_columns.py | 18 +- src/webgrid_tests/test_filters.py | 387 +++++---- src/webgrid_tests/test_rendering.py | 605 +++++++++----- src/webgrid_tests/test_testing.py | 103 +-- src/webgrid_tests/test_types.py | 20 +- src/webgrid_tests/test_unit.py | 336 ++++---- tox.ini | 15 +- 35 files changed, 2531 insertions(+), 1815 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index e745ddd..a448036 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -14,16 +14,16 @@ # import sys # sys.path.insert(0, os.path.abspath('.')) -import webgrid +import configparser import datetime as dt -import configparser +import webgrid # -- Project information ----------------------------------------------------- project = 'WebGrid' -copyright = u"{year} Level 12".format(year=dt.datetime.utcnow().year) +copyright = f'{dt.datetime.utcnow().year} Level 12' cfg = configparser.SafeConfigParser() @@ -78,12 +78,11 @@ 'Level 12': 'https://www.level12.io', 'File an Issue': 'https://github.com/level12/webgrid/issues/new', }, - 'show_powered_by': True - + 'show_powered_by': True, } # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] -html_css_files = ['css/custom.css'] \ No newline at end of file +html_css_files = ['css/custom.css'] diff --git a/noxfile.py b/noxfile.py index 9934145..89b8dac 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,7 +1,9 @@ from pathlib import Path + from nox import Session, options, parametrize from nox_uv import session + options.default_venv_backend = 'uv' package_path = Path.cwd() @@ -61,4 +63,3 @@ def precommit(session: Session): 'run', '--all-files', ) - diff --git a/src/webgrid/__init__.py b/src/webgrid/__init__.py index 60ef4c8..d642aa9 100644 --- a/src/webgrid/__init__.py +++ b/src/webgrid/__init__.py @@ -1,9 +1,7 @@ -from __future__ import absolute_import import datetime as dt import inspect import logging import sys -import six import time import urllib.parse @@ -11,8 +9,9 @@ from blazeutils.datastructures import BlankObject, OrderedDict from blazeutils.helpers import tolist from blazeutils.numbers import decimalfmt +from blazeutils.spreadsheets import openpyxl, xlsxwriter from blazeutils.strings import case_cw2us, randchars -from blazeutils.spreadsheets import xlsxwriter, openpyxl +import six import sqlalchemy as sa import sqlalchemy.sql as sasql @@ -21,6 +20,7 @@ from .renderers import HTML, XLSX from .version import VERSION as __version__ # noqa: F401 + # conditional imports to support libs without requiring them try: import arrow @@ -54,20 +54,17 @@ def subtotal_function_map(v): return v -class _None(object): +class _None: """ - A sentinal object to indicate no value + A sentinal object to indicate no value """ - pass class ExtractionError(TypeError): - """ raised when we are unable to extract a value from the record """ - pass + """raised when we are unable to extract a value from the record""" class _DeclarativeMeta(type): - def __new__(cls, name, bases, class_dict): class_dict['_rowstylers'] = [] class_dict['_colstylers'] = [] @@ -100,7 +97,7 @@ def __new__(cls, name, bases, class_dict): return super(_DeclarativeMeta, cls).__new__(cls, name, bases, class_dict) -class Column(object): +class Column: """Column represents the data and render specification for a table column. Args: @@ -134,6 +131,7 @@ class Column(object): xls_num_format (str, optional): Default numeric/date format type. """ + xls_width = None xls_num_format = None json_type_helper = None @@ -192,9 +190,20 @@ def _assign_to_grid(self): grid_cls_cols = grid_locals.setdefault('__cls_cols__', []) grid_cls_cols.append(self) - def __init__(self, label, key=None, filter=None, can_sort=True, # noqa: C901 - xls_width=None, xls_num_format=None, render_in=_None, has_subtotal=False, - visible=True, group=None, **kwargs): + def __init__( + self, + label, + key=None, + filter=None, + can_sort=True, + xls_width=None, + xls_num_format=None, + render_in=_None, + has_subtotal=False, + visible=True, + group=None, + **kwargs, + ): self.label = label self.key = key self.filter = filter @@ -236,17 +245,25 @@ def __init__(self, label, key=None, filter=None, can_sort=True, # noqa: C901 key = getattr(col, 'key', getattr(col, 'name', None)) if key is None: - raise ValueError(_('expected filter to be a SQLAlchemy column-like' - ' object, but it did not have a "key" or "name"' - ' attribute')) + raise ValueError( + _( + 'expected filter to be a SQLAlchemy column-like' + ' object, but it did not have a "key" or "name"' + ' attribute', + ), + ) self.key = self._query_key = key # filters can be sent in as a class (not class instance) if needed if inspect.isclass(filter): if self.expr is None: - raise ValueError(_('the filter was a class type, but no' - ' column-like object is available from "key" to pass in as' - ' as the first argument')) + raise ValueError( + _( + 'the filter was a class type, but no' + ' column-like object is available from "key" to pass in as' + ' as the first argument', + ), + ) self.filter = filter(self.expr) def new_instance(self, grid): @@ -258,9 +275,7 @@ def new_instance(self, grid): """ cls = self.__class__ key = grid.get_unique_column_key( - self.key - or case_cw2us(str(self.label).replace(' ', '')) - or 'unnamed_expression' + self.key or case_cw2us(str(self.label).replace(' ', '')) or 'unnamed_expression', ) column = cls(self.label, key, None, self.can_sort, group=self.group, _dont_assign=True) @@ -271,7 +286,8 @@ def new_instance(self, grid): if self.filter: column.filter = self.filter.new_instance( - dialect=grid.manager.db.engine.dialect, col=column.expr + dialect=grid.manager.db.engine.dialect, + col=column.expr, ) column.head = BlankObject() @@ -282,13 +298,18 @@ def new_instance(self, grid): # try to be smart about which attributes should get copied to the # new instance by looking for attributes on the class that have the # same name as arguments to the classes __init__ method - args = (inspect.getargspec(self.__init__).args - if six.PY2 else inspect.getfullargspec(self.__init__).args) + args = ( + inspect.getargspec(self.__init__).args + if six.PY2 + else inspect.getfullargspec(self.__init__).args + ) for argname in args: - if argname != 'self' and argname not in ( - 'label', 'key', 'filter', 'can_sort', 'render_in', 'visible' - ) and hasattr(self, argname): + if ( + argname != 'self' + and argname not in ('label', 'key', 'filter', 'can_sort', 'render_in', 'visible') + and hasattr(self, argname) + ): setattr(column, argname, getattr(self, argname)) # Copy underlying value of render_in and visible, in case they are @@ -300,8 +321,8 @@ def new_instance(self, grid): def extract_and_format_data(self, record): """ - Extract a value from the record for this column and run it through - the data formaters. + Extract a value from the record for this column and run it through + the data formaters. """ data = self.extract_data(record) data = self.format_data(data) @@ -311,9 +332,9 @@ def extract_and_format_data(self, record): data = _filter(self.grid, data) return data - def extract_data(self, record): # noqa: C901 + def extract_data(self, record): """ - Locate the data for this column in the record and return it. + Locate the data for this column in the record and return it. """ # key style based on key try: @@ -324,10 +345,7 @@ def extract_data(self, record): # noqa: C901 pass # index style based on position in query and key - if ( - self._query_idx is not None - and hasattr(record, '_fields') - ): + if self._query_idx is not None and hasattr(record, '_fields'): try: if record._fields[self._query_idx] == self._query_key: return record[self._query_idx] @@ -355,8 +373,8 @@ def extract_data(self, record): # noqa: C901 def format_data(self, value): """ - Use to adjust the value extracted from the record for this column. - By default, no change is made. Useful in sub-classes. + Use to adjust the value extracted from the record for this column. + By default, no change is made. Useful in sub-classes. """ return value @@ -369,7 +387,7 @@ def render(self, render_type, record, *args, **kwargs): Renderer-specific methods are expected to be named `render_`, e.g. `render_html` or `render_xlsx`. """ - render_attr = 'render_{0}'.format(render_type) + render_attr = f'render_{render_type}' if hasattr(self, render_attr): return getattr(self, render_attr)(record, *args, **kwargs) return self.extract_and_format_data(record) @@ -378,13 +396,13 @@ def apply_sort(self, query, flag_desc): """Query modifier to enable sort for this column's expression.""" if self.expr is None: direction = 'DESC' if flag_desc else 'ASC' - return query.order_by(sasql.text('{0} {1}'.format(self.key, direction))) + return query.order_by(sasql.text(f'{self.key} {direction}')) if flag_desc: return query.order_by(self.expr.desc()) return query.order_by(self.expr) def __repr__(self): - return ''.format(self) + return f'' def xls_width_calc(self, value): """Calculate a width to use for an Excel renderer. @@ -411,14 +429,37 @@ class LinkColumnBase(Column): link_attrs (dict): Additional attributes to render on the A tag. """ + link_attrs = {} - def __init__(self, label, key=None, filter=None, can_sort=True, - link_label=None, xls_width=None, xls_num_format=None, - render_in=_None, has_subtotal=False, visible=True, group=None, **kwargs): - super().__init__(label, key, filter, can_sort, xls_width, - xls_num_format, render_in, has_subtotal, visible, - group=group, **kwargs) + def __init__( + self, + label, + key=None, + filter=None, + can_sort=True, + link_label=None, + xls_width=None, + xls_num_format=None, + render_in=_None, + has_subtotal=False, + visible=True, + group=None, + **kwargs, + ): + super().__init__( + label, + key, + filter, + can_sort, + xls_width, + xls_num_format, + render_in, + has_subtotal, + visible, + group=group, + **kwargs, + ) self.link_label = link_label def link_to(self, label, url, **kwargs): @@ -427,7 +468,7 @@ def link_to(self, label, url, **kwargs): '{{label}}', url=url, attrs=kwargs, - label=label + label=label, ) def render_html(self, record, hah): @@ -457,15 +498,39 @@ class BoolColumn(Column): false_label (str, optional): String to use for the false case. """ + json_type_helper = 'boolean' - def __init__(self, label, key_or_filter=None, key=None, can_sort=True, - reverse=False, true_label=_('True'), false_label=_('False'), - xls_width=None, xls_num_format=None, render_in=_None, has_subtotal=False, - visible=True, group=None, **kwargs): - super().__init__(label, key_or_filter, key, can_sort, xls_width, - xls_num_format, render_in, - has_subtotal, visible, group=group, **kwargs) + def __init__( + self, + label, + key_or_filter=None, + key=None, + can_sort=True, + reverse=False, + true_label=_('True'), + false_label=_('False'), + xls_width=None, + xls_num_format=None, + render_in=_None, + has_subtotal=False, + visible=True, + group=None, + **kwargs, + ): + super().__init__( + label, + key_or_filter, + key, + can_sort, + xls_width, + xls_num_format, + render_in, + has_subtotal, + visible, + group=group, + **kwargs, + ) self.reverse = reverse self.true_label = true_label self.false_label = false_label @@ -486,12 +551,37 @@ class YesNoColumn(BoolColumn): """ - def __init__(self, label, key_or_filter=None, key=None, can_sort=True, - reverse=False, xls_width=None, xls_num_format=None, render_in=_None, - has_subtotal=False, visible=True, group=None, **kwargs): - super().__init__(label, key_or_filter, key, can_sort, reverse, - _('Yes'), _('No'), xls_width, xls_num_format, - render_in, has_subtotal, visible, group=group, **kwargs) + def __init__( + self, + label, + key_or_filter=None, + key=None, + can_sort=True, + reverse=False, + xls_width=None, + xls_num_format=None, + render_in=_None, + has_subtotal=False, + visible=True, + group=None, + **kwargs, + ): + super().__init__( + label, + key_or_filter, + key, + can_sort, + reverse, + _('Yes'), + _('No'), + xls_width, + xls_num_format, + render_in, + has_subtotal, + visible, + group=group, + **kwargs, + ) class DateColumnBase(Column): @@ -508,13 +598,35 @@ class DateColumnBase(Column): """ - def __init__(self, label, key_or_filter=None, key=None, can_sort=True, - html_format=None, csv_format=None, xls_width=None, - xls_num_format=None, render_in=_None, has_subtotal=False, visible=True, group=None, - **kwargs): - super().__init__(label, key_or_filter, key, can_sort, xls_width, - xls_num_format, render_in, has_subtotal, - visible, group=group, **kwargs) + def __init__( + self, + label, + key_or_filter=None, + key=None, + can_sort=True, + html_format=None, + csv_format=None, + xls_width=None, + xls_num_format=None, + render_in=_None, + has_subtotal=False, + visible=True, + group=None, + **kwargs, + ): + super().__init__( + label, + key_or_filter, + key, + can_sort, + xls_width, + xls_num_format, + render_in, + has_subtotal, + visible, + group=group, + **kwargs, + ) if html_format: self.html_format = html_format @@ -584,6 +696,7 @@ class DateColumn(DateColumnBase): xls_num_format (str, optional): Date format string for Excel. """ + # !!!: localize html_format = '%m/%d/%Y' csv_format = '%Y-%m-%d' @@ -604,6 +717,7 @@ class DateTimeColumn(DateColumnBase): xls_num_format (str, optional): Date format string for Excel. """ + # !!!: localize html_format = '%m/%d/%Y %I:%M %p' csv_format = '%Y-%m-%d %H:%M:%S%z' @@ -624,6 +738,7 @@ class TimeColumn(DateColumnBase): xls_num_format (str, optional): Date format string for Excel. """ + # !!!: localize html_format = '%I:%M %p' csv_format = '%H:%M' @@ -660,20 +775,51 @@ class NumericColumn(Column): `xls_fmt_general`, `xls_fmt_accounting`, `xls_fmt_percent` are Excel number formats used for the corresponding `format_as` setting. """ + # !!!: localize xls_fmt_general = '#,##0{dec_places};{neg_prefix}-#,##0{dec_places}' - xls_fmt_accounting = '_($* #,##0{dec_places}_);{neg_prefix}_($* (#,##0{dec_places})' + \ - ';_($* "-"??_);_(@_)' + xls_fmt_accounting = ( + '_($* #,##0{dec_places}_);{neg_prefix}_($* (#,##0{dec_places})' + ';_($* "-"??_);_(@_)' + ) xls_fmt_percent = '0{dec_places}%;{neg_prefix}-0{dec_places}%' - def __init__(self, label, key_or_filter=None, key=None, can_sort=True, - reverse=False, xls_width=None, xls_num_format=None, - render_in=_None, format_as='general', places=2, curr='', - sep=',', dp='.', pos='', neg='-', trailneg='', - xls_neg_red=True, has_subtotal=False, visible=True, group=None, **kwargs): - super().__init__(label, key_or_filter, key, can_sort, xls_width, - xls_num_format, render_in, - has_subtotal, visible, group=group, **kwargs) + def __init__( + self, + label, + key_or_filter=None, + key=None, + can_sort=True, + reverse=False, + xls_width=None, + xls_num_format=None, + render_in=_None, + format_as='general', + places=2, + curr='', + sep=',', + dp='.', + pos='', + neg='-', + trailneg='', + xls_neg_red=True, + has_subtotal=False, + visible=True, + group=None, + **kwargs, + ): + super().__init__( + label, + key_or_filter, + key, + can_sort, + xls_width, + xls_num_format, + render_in, + has_subtotal, + visible, + group=group, + **kwargs, + ) self.places = places self.curr = curr self.sep = sep @@ -744,9 +890,7 @@ def get_num_format(self): @property def xlsx_style(self): """Number format for XLSX target.""" - return { - 'num_format': self.get_num_format() - } + return {'num_format': self.get_num_format()} class EnumColumn(Column): @@ -754,19 +898,21 @@ class EnumColumn(Column): This column type is meant to be used with python `enum.Enum` type columns. It expects that the display value is the `value` attribute of the enum instance. """ + def format_data(self, value): if value is None: return None return value.value -class ColumnGroup(object): +class ColumnGroup: r"""Represents a grouping of grid columns which may be rendered within a group label. Args: label (str): Grouping label to be rendered for the column set. class\_ (str): CSS class name to apply in HTML rendering. - """ # noqa: W605 + """ + label = None class_ = None @@ -825,10 +971,12 @@ def args_filter(self): continue grid_args.append((f'op({col.key})', _filter.op)) if _filter.value1: - grid_args.extend(map( - lambda item: (f'v1({col.key})', item), - tolist(_filter.value1), - )) + grid_args.extend( + map( + lambda item: (f'v1({col.key})', item), + tolist(_filter.value1), + ), + ) if _filter.value2: grid_args.append((f'v2({col.key})', _filter.value2)) return grid_args @@ -914,7 +1062,8 @@ class BaseGrid(six.with_metaclass(_DeclarativeMeta, object)): are not set on the grid. Note, relationship attributes must be referenced within tuples, due to SQLAlchemy magic. - """ # noqa: W605 + """ + __cls_cols__ = () identifier = None sorter_on = True @@ -951,8 +1100,15 @@ class BaseGrid(six.with_metaclass(_DeclarativeMeta, object)): # Set to None to disable this check unconfirmed_export_limit = 10000 - def __init__(self, ident=None, per_page=_None, on_page=_None, qs_prefix='', class_='datagrid', - **kwargs): + def __init__( + self, + ident=None, + per_page=_None, + on_page=_None, + qs_prefix='', + class_='datagrid', + **kwargs, + ): self._ident = ident self.hah = HTMLAttributes(kwargs) self.hah.id = self.ident @@ -1011,25 +1167,17 @@ def add_column(self, column): if new_col.filter is not None: self.filtered_cols[new_col.key] = new_col if new_col.has_subtotal is not False and new_col.has_subtotal is not None: - self.subtotal_cols[new_col.key] = ( - subtotal_function_map(new_col.has_subtotal), - new_col - ) + self.subtotal_cols[new_col.key] = (subtotal_function_map(new_col.has_subtotal), new_col) def drop_columns(self, column_keys): - self.columns = list(filter( - lambda col: col.key not in tolist(column_keys), - self.columns - )) + self.columns = list(filter(lambda col: col.key not in tolist(column_keys), self.columns)) for key in tolist(column_keys): self.key_column_map.pop(key, None) self.filtered_cols.pop(key, None) self.subtotal_cols.pop(key, None) def post_init(self): - """Provided for subclasses to run post-initialization customizations. - """ - pass + """Provided for subclasses to run post-initialization customizations.""" def set_column_order(self, column_keys): """Most renderers output columns in the order they appear in the grid's ``columns`` @@ -1041,14 +1189,10 @@ def set_column_order(self, column_keys): if key_check: raise Exception(f'Keys not recognized on grid: {key_check}') - self.columns = [ - self.key_column_map[key] for key in column_keys - ] + self.columns = [self.key_column_map[key] for key in column_keys] def before_query_hook(self): - """Hook to give subclasses a chance to change things before executing the query. - """ - pass + """Hook to give subclasses a chance to change things before executing the query.""" def build(self, grid_args=None): """Apply query args, run `before_query_hook`, and execute a record count query. @@ -1072,7 +1216,6 @@ def check_auth(self): Note, this method is not part of normal grid/render operation. It will only be executed if run by a calling layer, such as the Flask WebGridAPI manager/extension. """ - pass def column(self, ident): """Retrieve a grid column instance via either the key string or index int. @@ -1127,7 +1270,7 @@ def get_unique_column_key(self, key): new_key = key while self.has_column(new_key): suffix_counter += 1 - new_key = '{}_{}'.format(key, suffix_counter) + new_key = f'{key}_{suffix_counter}' return new_key def iter_columns(self, render_type): @@ -1167,16 +1310,21 @@ def search_expression_generators(self): def check_expression_generator(expr_gen): if expr_gen is not None and not callable(expr_gen): raise Exception( - 'bad filter search expression: {} is not callable'.format(str(expr_gen)) + f'bad filter search expression: {str(expr_gen)} is not callable', ) return expr_gen is not None # Also conditionally filter aggregate/non-aggregate so we're not mixing expression types. - return tuple(filter( - check_expression_generator, - [col.filter.get_search_expr() for col in self.filtered_cols.values() - if col.filter.is_aggregate is is_aggregate] - )) + return tuple( + filter( + check_expression_generator, + [ + col.filter.get_search_expr() + for col in self.filtered_cols.values() + if col.filter.is_aggregate is is_aggregate + ], + ), + ) @property def search_uses_aggregate(self): @@ -1199,8 +1347,7 @@ def search_uses_aggregate(self): return has_search def set_renderers(self): - """Renderers assigned as attributes on the grid instance, named by render target. - """ + """Renderers assigned as attributes on the grid instance, named by render target.""" self.html = HTML(self) for key, value in self.allowed_export_targets.items(): setattr(self, key, value(self)) @@ -1269,9 +1416,7 @@ def clear_record_cache(self, preserve_count=False): @property def ident(self): - return self._ident \ - or self.identifier \ - or case_cw2us(self.__class__.__name__) + return self._ident or self.identifier or case_cw2us(self.__class__.__name__) @property def default_session_key(self): @@ -1313,7 +1458,7 @@ def record_count(self): t0 = time.perf_counter() self._record_count = query.count() t1 = time.perf_counter() - log.debug('Count query ran in {} seconds'.format(t1 - t0)) + log.debug(f'Count query ran in {t1 - t0} seconds') return self._record_count @property @@ -1331,7 +1476,7 @@ def records(self): t0 = time.perf_counter() self._records = query.all() t1 = time.perf_counter() - log.debug('Data query ran in {} seconds'.format(t1 - t0)) + log.debug(f'Data query ran in {t1 - t0} seconds') return self._records def _totals_col_results(self, page_totals_only): @@ -1354,17 +1499,13 @@ def _totals_col_results(self, page_totals_only): # can be applied. # This will apply to any columns with an expr. Other subtotaled columns can be # tacked onto the end - these will not be indexed and must be referred to by name - for colobj in [ - col for col in self.columns if col.expr is not None - ] + [ + for colobj in [col for col in self.columns if col.expr is not None] + [ coltuple[1] for _, coltuple in self.subtotal_cols.items() if coltuple[1].expr is None ]: colname = colobj._query_key or colobj.key if colobj.key not in self.subtotal_cols: - cols.append( - sa.literal(None).label(colname) - ) + cols.append(sa.literal(None).label(colname)) continue sa_aggregate_func, _ = self.subtotal_cols[colobj.key] @@ -1395,7 +1536,7 @@ def _totals_col_results(self, page_totals_only): query._set_select_from([SUB], True) result = query.first() t1 = time.perf_counter() - log.debug('Totals query ran in {} seconds'.format(t1 - t0)) + log.debug(f'Totals query ran in {t1 - t0} seconds') return result @@ -1412,11 +1553,7 @@ def page_totals(self): Returns: Any: Single result record, or None if page totals are not configured. """ - if ( - self._page_totals is None - and self.subtotals in ('page', 'all') - and self.subtotal_cols - ): + if self._page_totals is None and self.subtotals in ('page', 'all') and self.subtotal_cols: self._page_totals = self._totals_col_results(page_totals_only=True) return self._page_totals @@ -1433,18 +1570,13 @@ def grand_totals(self): Returns: Any: Single result record, or None if grand totals are not configured. """ - if ( - self._grand_totals is None - and self.subtotals in ('grand', 'all') - and self.subtotal_cols - ): + if self._grand_totals is None and self.subtotals in ('grand', 'all') and self.subtotal_cols: self._grand_totals = self._totals_col_results(page_totals_only=False) return self._grand_totals @property def page_count(self): - """Page count, or 1 if no `per_page` is set. - """ + """Page count, or 1 if no `per_page` is set.""" if self.per_page is None: return 1 return max(0, self.record_count - 1) // self.per_page + 1 @@ -1524,10 +1656,10 @@ def query_base(self, has_sort, has_filters): if self.query_select_from is not None: query = query.select_from(*tolist(self.query_select_from)) - for join_terms in (tolist(self.query_joins) or tuple()): + for join_terms in tolist(self.query_joins) or tuple(): query = query.join(*tolist(join_terms)) - for join_terms in (tolist(self.query_outer_joins) or tuple()): + for join_terms in tolist(self.query_outer_joins) or tuple(): query = query.outerjoin(*tolist(join_terms)) if self.query_filter: @@ -1574,7 +1706,7 @@ def query_filters(self, query): for col in six.itervalues(self.filtered_cols): if col.filter.is_active: - filter_display.append('{}: {}'.format(col.key, str(col.filter))) + filter_display.append(f'{col.key}: {str(col.filter)}') query = col.filter.apply(query) if filter_display: log.debug(';'.join(filter_display)) @@ -1596,10 +1728,12 @@ def apply_search(self, query, value): Query: SQLAlchemy query """ filter_method = query.having if self.search_uses_aggregate else query.filter - filter_clauses = list(filter( - lambda item: item is not None, - (expr(value) for expr in self.search_expression_generators) - )) + filter_clauses = list( + filter( + lambda item: item is not None, + (expr(value) for expr in self.search_expression_generators), + ), + ) if not filter_clauses: return query @@ -1617,7 +1751,7 @@ def query_paging(self, query): if self.on_page and self.per_page: offset = (self.on_page - 1) * self.per_page query = query.offset(offset).limit(self.per_page) - log.debug('Page {}; {} per page'.format(self.on_page, self.per_page)) + log.debug(f'Page {self.on_page}; {self.per_page} per page') return query def query_sort(self, query): @@ -1669,7 +1803,7 @@ def _fix_mssql_order_by(self, query): if query._order_by_clauses: return query raise Exception( - 'Paging is enabled, but query does not have ORDER BY clause required for MSSQL' + 'Paging is enabled, but query does not have ORDER BY clause required for MSSQL', ) def build_qs_args(self, include_session=False): @@ -1722,10 +1856,7 @@ def apply_qs_args(self, add_user_warnings=True, grid_args=None): self.manager.flash_message('warning', msg) def _apply_search(self, args): - if ( - 'search' in args - and self.can_search() - ): + if 'search' in args and self.can_search(): self.search_value = args['search'].strip() if args['search'] else None def _apply_filtering(self, args): @@ -1736,9 +1867,9 @@ def _apply_filtering(self, args): """ for col in six.itervalues(self.filtered_cols): filter = col.filter - filter_op_qsk = 'op({0})'.format(col.key) - filter_v1_qsk = 'v1({0})'.format(col.key) - filter_v2_qsk = 'v2({0})'.format(col.key) + filter_op_qsk = f'op({col.key})' + filter_v1_qsk = f'v1({col.key})' + filter_v2_qsk = f'v2({col.key})' filter_op_value = args.get(filter_op_qsk, None) @@ -1802,7 +1933,7 @@ def _apply_sorting(self, args): while True: counter += 1 - sort_arg = 'sort{}'.format(counter) + sort_arg = f'sort{counter}' if sort_arg not in args: break sort_qs_values.append(args[sort_arg]) @@ -1827,7 +1958,7 @@ def prefix_qs_arg_key(self, key): Returns: str: Prefixed arg key. """ - return '{0}{1}'.format(self.qs_prefix, key) + return f'{self.qs_prefix}{key}' def apply_validator(self, validator, value, qs_arg_key): """Apply a webgrid validator to value, and produce a warning if invalid. @@ -1877,7 +2008,7 @@ def export_as_response(self, wb=None, sheet_name=None): return exporter.as_response() def __repr__(self): - return ''.format(self.__class__.__name__) + return f'' def row_styler(f): @@ -1889,6 +2020,7 @@ def col_styler(for_column): def decorator(f): f.__grid_colstyler__ = for_column return f + return decorator @@ -1896,4 +2028,5 @@ def col_filter(for_column): def decorator(f): f.__grid_colfilter__ = for_column return f + return decorator diff --git a/src/webgrid/blazeweb.py b/src/webgrid/blazeweb.py index 8c41540..407d798 100644 --- a/src/webgrid/blazeweb.py +++ b/src/webgrid/blazeweb.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from blazeweb.content import getcontent from blazeweb.globals import ag, rg, user from blazeweb.routing import abs_static_url @@ -8,6 +6,7 @@ from blazeweb.wrappers import StreamResponse from jinja2.exceptions import TemplateNotFound from sqlalchemybwc import db as sabwc_db + from webgrid import BaseGrid from webgrid.extensions import FrameworkManager, gettext from webgrid.renderers import render_html_attributes @@ -54,13 +53,13 @@ def request(self): return rg.request def static_url(self, url_tail): - return abs_static_url('component/webgrid/{0}'.format(url_tail)) + return abs_static_url(f'component/webgrid/{url_tail}') def file_as_response(self, data_stream, file_name, mime_type): rp = StreamResponse(data_stream) rp.headers['Content-Type'] = mime_type if file_name is not None: - rp.headers['Content-Disposition'] = 'attachment; filename={}'.format(file_name) + rp.headers['Content-Disposition'] = f'attachment; filename={file_name}' abort(rp) def render_template(self, endpoint, **kwargs): @@ -69,8 +68,10 @@ def render_template(self, endpoint, **kwargs): except TemplateNotFound: if ':' in endpoint: raise - return getcontent('{0}:{1}'.format(self.component, endpoint), **kwargs) -wg_blaze_manager = WebGrid() # noqa: E305 + return getcontent(f'{self.component}:{endpoint}', **kwargs) + + +wg_blaze_manager = WebGrid() class Grid(BaseGrid): diff --git a/src/webgrid/extensions.py b/src/webgrid/extensions.py index e3938ff..263711f 100644 --- a/src/webgrid/extensions.py +++ b/src/webgrid/extensions.py @@ -13,6 +13,7 @@ from . import types + MORPHI_PACKAGE_NAME = 'webgrid' # begin morphi boilerplate @@ -68,12 +69,13 @@ def default(self, obj): class ArgsLoader(ABC): - """ Base args loader class. + """Base args loader class. When a grid calls for its args, it requests them from the manager. The manager will have one or more args loaders to be run in order. Each loader fetches its args from the request, then ensuing loaders have the opportunity to modify or perform other operations as needed. """ + def __init__(self, manager): self.manager = manager @@ -85,7 +87,7 @@ def get_args(self, grid, previous_args): class GridPrefixBase(ArgsLoader): def sanitize_arg_name(self, arg_name, qs_prefix): if qs_prefix and arg_name.startswith(qs_prefix): - return arg_name[len(qs_prefix):] + return arg_name[len(qs_prefix) :] return arg_name def get_sanitized_args(self, grid, args): @@ -120,7 +122,7 @@ def get_args_from_request(self): class RequestArgsLoader(GridPrefixBase, ArgsLoader): - """ Simple args loader for web request. + """Simple args loader for web request. Args are usually passed through directly from the request. If the grid has a query string prefix, the relevant args will be namespaced - sanitize them here and return the subset @@ -128,12 +130,13 @@ class RequestArgsLoader(GridPrefixBase, ArgsLoader): In the reset case, ignore most args, and return only the reset flag and session key (if any). """ + def get_args_from_request(self): return self.manager.request_url_args() class RequestFormLoader(GridPrefixBase, ArgsLoader): - """ Simple form loader for web request. + """Simple form loader for web request. Form values are usually passed through directly from the request. If the grid has a prefix, the relevant args will be namespaced - sanitize them here and return the subset needed for the given @@ -141,17 +144,19 @@ class RequestFormLoader(GridPrefixBase, ArgsLoader): In the reset case, ignore most args, and return only the reset flag and session key (if any). """ + def get_args_from_request(self): return self.manager.request_form_args() class RequestJsonLoader(ArgsLoader): - """ JSON loader for web request. + """JSON loader for web request. See :meth:`webgrid.types.GridSettings` for the expected JSON structure. The parsed arguments are converted to the querystring arg format and merged with any previous args. """ - def json_to_args(self, data: Dict[str, Any]): + + def json_to_args(self, data: dict[str, Any]): meta = types.GridSettings.from_dict(data) return MultiDict(meta.to_args()) @@ -166,7 +171,7 @@ def get_args(self, grid, previous_args): class WebSessionArgsLoader(ArgsLoader): - """ Session manager for grid args. + """Session manager for grid args. Args are assumed to have been sanitized from the request already. But, args may be combined from the request and the session store for a number of cases. @@ -179,6 +184,7 @@ class WebSessionArgsLoader(ArgsLoader): In the reset case, ignore most args, and return only the reset flag and session key (if any). And clear the session store for the given grid. """ + _session_exclude_keys = ( '__foreign_session_loaded__', 'apply', @@ -223,7 +229,8 @@ def args_have_sort(self, args): """ regex = re.compile('(sort[1-9][0-9]*)') return [ - arg.string for arg in filter( + arg.string + for arg in filter( lambda match: match is not None, [regex.match(a) for a in args], ) @@ -240,7 +247,7 @@ def remove_grid_session(self, session_key): self.manager.persist_web_session() def apply_session_overrides(self, session_args, previous_args): - """ Update session args as needed from the incoming request. + """Update session args as needed from the incoming request. If session override case, wholesale update from the incoming request. This is useful if a single filter needs to be changed via the URL, but we don't @@ -368,8 +375,9 @@ def save_session_store(self, grid, args): return self.manager.persist_web_session() args['datagrid'] = grid.default_session_key - args['session_timestamp'] = existing_default_store['session_timestamp'] \ - = arrow.utcnow().isoformat() + args['session_timestamp'] = existing_default_store['session_timestamp'] = ( + arrow.utcnow().isoformat() + ) # if we're pulling a grid matching the default session, but with a different key, # no need to store the sepearate session @@ -387,7 +395,7 @@ def save_session_store(self, grid, args): self.manager.persist_web_session() def get_args(self, grid, previous_args): - """ Retrieve args from session and override as appropriate. + """Retrieve args from session and override as appropriate. Submitting the header form flushes all args to the URL, so no need to load them from session. @@ -428,7 +436,8 @@ def get_args(self, grid, previous_args): if (not self.args_have_op(request_args) and not is_apply) or is_override: # We are in a session-loading case. session_args = self.apply_session_overrides( - self.get_session_store(grid, request_args), request_args + self.get_session_store(grid, request_args), + request_args, ) # Flag a foreign session if loading from another grid's session. @@ -459,6 +468,7 @@ class FrameworkManager(ABC): disable. Default 12. """ + jinja_loader = lambda self: jinja.PackageLoader('webgrid', 'templates') args_loaders = ( RequestArgsLoader, @@ -491,7 +501,7 @@ def init_jinja(self): self.jinja_environment = jinja.Environment( loader=self.jinja_loader, finalize=lambda x: x if x is not None else '', - autoescape=True + autoescape=True, ) def static_path(self): @@ -513,7 +523,8 @@ def test_request_context(self, url='/'): def request_args(self): warnings.warn( 'request_args is deprecated and will be removed in a future version.', - DeprecationWarning, 2 + DeprecationWarning, + 2, ) return self.request_url_args() diff --git a/src/webgrid/filters.py b/src/webgrid/filters.py index dfce0bc..3ed134c 100644 --- a/src/webgrid/filters.py +++ b/src/webgrid/filters.py @@ -1,4 +1,3 @@ -from __future__ import absolute_import import calendar import datetime as dt from decimal import Decimal as D @@ -7,17 +6,16 @@ from blazeutils import tolist from blazeutils.dates import ensure_date, ensure_datetime from dateutil.parser import parse -from dateutil.relativedelta import relativedelta, SU -from sqlalchemy.sql import or_, and_ -import sqlalchemy as sa +from dateutil.relativedelta import SU, relativedelta import six +import sqlalchemy as sa +from sqlalchemy.sql import and_, or_ from werkzeug.datastructures import ImmutableDict -from .extensions import ( - gettext, - lazy_gettext as _ -) from . import types, validators +from .extensions import gettext +from .extensions import lazy_gettext as _ + try: import arrow @@ -33,7 +31,7 @@ class UnrecognizedOperator(ValueError): pass -class Operator(object): +class Operator: """Filter operator representing name and potential inputs. See webgrid.filters.ops for a collection of predefined operators. @@ -52,6 +50,7 @@ class Operator(object): hint (str, optional): Input field hint to show in UI. Defaults to None. """ + def __init__(self, key, display, field_type, hint=None): self.key = key self.display = display @@ -65,7 +64,7 @@ def __hash__(self): return hash(self.key) -class ops(object): +class ops: eq = Operator('eq', _('is'), 'input') not_eq = Operator('!eq', _('is not'), 'input') is_ = Operator('is', _('is'), 'select') @@ -73,7 +72,7 @@ class ops(object): empty = Operator('empty', _('empty'), None) not_empty = Operator('!empty', _('not empty'), None) contains = Operator('contains', _('contains'), 'input') - not_contains = Operator('!contains', _('doesn\'t contain'), 'input') + not_contains = Operator('!contains', _("doesn't contain"), 'input') less_than_equal = Operator('lte', _('less than or equal'), 'input') greater_than_equal = Operator('gte', _('greater than or equal'), 'input') between = Operator('between', _('between'), '2inputs') @@ -95,7 +94,7 @@ class ops(object): this_year = Operator('thisyear', _('this year'), None) -class FilterBase(object): +class FilterBase: """Base filter class interface for webgrid filters. Contains filter operators, inputs, render information, and the inner workings to apply @@ -143,6 +142,7 @@ class FilterBase(object): error (bool): True if input processing encountered a validation error. """ + operators = ops.eq, ops.not_eq, ops.empty, ops.not_empty # one operator may be specified as the "primary", i.e. the one to select when filter is added # note, the renderer is responsible for using this operator @@ -152,7 +152,7 @@ class FilterBase(object): # new_instance() init_attrs_for_instance = () # current HTML renderer allows for "input", "input2", and/or "select" - input_types = 'input', + input_types = ('input',) # match operators to the HTML5 type(s) html_input_types = {} # does this filter take a list of values in it's set() method @@ -160,8 +160,14 @@ class FilterBase(object): # does this filter apply to the HAVING clause is_aggregate = False - def __init__(self, sa_col=None, default_op=None, default_value1=None, default_value2=None, - dialect=None): + def __init__( + self, + sa_col=None, + default_op=None, + default_value1=None, + default_value2=None, + dialect=None, + ): # attributes from static instance self.sa_col = sa_col self._default_op = default_op @@ -186,14 +192,17 @@ def __init__(self, sa_col=None, default_op=None, default_value1=None, default_va # Can't use inspect.stack() here because when called from within a Jinja template, # inspect.getframeinfo raises an exception while frame: - if frame.f_code.co_name != '__init__' or \ - not isinstance(frame.f_locals.get('self'), FilterBase): + if frame.f_code.co_name != '__init__' or not isinstance( + frame.f_locals.get('self'), + FilterBase, + ): break outermost = inspect.getargvalues(frame) frame = frame.f_back - self._vargs = [outermost.locals[a] for a in outermost.args[1:]] + \ - list(outermost.locals[outermost.varargs] if outermost.varargs else []) + self._vargs = [outermost.locals[a] for a in outermost.args[1:]] + list( + outermost.locals[outermost.varargs] if outermost.varargs else [], + ) self._kwargs = outermost.locals[outermost.keywords] if outermost.keywords else {} @@ -201,8 +210,10 @@ def __init__(self, sa_col=None, default_op=None, default_value1=None, default_va def is_active(self): """Filter is active if op is set and input requirements are met.""" operator_by_key = {op.key: op for op in self.operators} - return self.op is not None and not self.error and ( - operator_by_key[self.op].field_type is None or self.value1 is not None + return ( + self.op is not None + and not self.error + and (operator_by_key[self.op].field_type is None or self.value1 is not None) ) @property @@ -239,7 +250,7 @@ def set(self, op, value1, value2=None): if not op: self.default_op = self._default_op() if callable(self._default_op) else self._default_op self.op = self.default_op - self.using_default_op = (self.default_op is not None) + self.using_default_op = self.default_op is not None if self.op is None: return @@ -287,28 +298,25 @@ def apply(self, query): def process(self, value, is_value2): """ - Process the values as given to .set(), validating and manipulating - as needed. + Process the values as given to .set(), validating and manipulating + as needed. """ return value def format_invalid(self, exc, col): """Wrapper for generating a validation error string.""" - return '{0}: {1}'.format( - col.label, - str(exc) - ) + return f'{col.label}: {str(exc)}' def get_search_expr(self): """ - Filters can be used for the general "single search" function on the grid. For this to - work in SQL, the grid needs to pull search expressions from all filters and OR them - together. + Filters can be used for the general "single search" function on the grid. For this to + work in SQL, the grid needs to pull search expressions from all filters and OR them + together. - Return value is expected to be a callable taking one argument (the search value). - E.g. `lambda value: self.sa_col.like('%{}%'.format(value))` + Return value is expected to be a callable taking one argument (the search value). + E.g. `lambda value: self.sa_col.like('%{}%'.format(value))` - Return value of `None` is filtered out, essentially disabling search for the filter. + Return value of `None` is filtered out, essentially disabling search for the filter. """ return None @@ -332,8 +340,7 @@ def serialize_filter_option(self, key, value): def serialize_filter_spec(self): return types.FilterSpec( operators=[self.serialize_filter_operator(op) for op in self.operators], - primary_op=self.serialize_filter_operator( - self.primary_op) if self.primary_op else None, + primary_op=self.serialize_filter_operator(self.primary_op) if self.primary_op else None, ) def new_instance(self, **kwargs): @@ -349,12 +356,10 @@ def new_instance(self, **kwargs): return new_filter def __repr__(self): - return 'class={}, op={}, value1={}, value2={}'.format( - self.__class__.__name__, self.op, self.value1, self.value2 - ) + return f'class={self.__class__.__name__}, op={self.op}, value1={self.value1}, value2={self.value2}' -class _NoValue(object): +class _NoValue: pass @@ -386,10 +391,21 @@ class attribute, but can be overridden as an instance attribute, property, or me receives_list = True options_from = () - def __init__(self, sa_col, value_modifier='auto', default_op=None, default_value1=None, - default_value2=None): - FilterBase.__init__(self, sa_col, default_op=default_op, default_value1=default_value1, - default_value2=default_value2) + def __init__( + self, + sa_col, + value_modifier='auto', + default_op=None, + default_value1=None, + default_value2=None, + ): + FilterBase.__init__( + self, + sa_col, + default_op=default_op, + default_value1=default_value1, + default_value2=default_value2, + ) # attributes from static instance self.value_modifier = value_modifier @@ -402,9 +418,7 @@ def serialize_filter_spec(self): return types.OptionsFilterSpec( operators=base_spec.operators, primary_op=base_spec.primary_op, - options=[ - self.serialize_filter_option(key, value) - for key, value in self.options_seq], + options=[self.serialize_filter_option(key, value) for key, value in self.options_seq], ) def new_instance(self, **kwargs): @@ -441,9 +455,14 @@ def setup_validator(self): # a set() operation should be converted to if self.value_modifier == 'auto' or self.value_modifier is None: if self.value_modifier and len(self.option_keys) == 0: - raise ValueError(_('value_modifier argument set to "auto", but ' - 'the options set is empty and the type can therefore not ' - 'be determined for {name}', name=self.__class__.__name__)) + raise ValueError( + _( + 'value_modifier argument set to "auto", but ' + 'the options set is empty and the type can therefore not ' + 'be determined for {name}', + name=self.__class__.__name__, + ), + ) first_key = self.option_keys[0] if isinstance(first_key, six.string_types) or self.value_modifier is None: self.value_modifier = validators.StringValidator() @@ -455,8 +474,10 @@ def setup_validator(self): self.value_modifier = validators.DecimalValidator() else: raise TypeError( - _("can't use value_modifier='auto' when option keys are {key_type}", - key_type=type(first_key)) + _( + "can't use value_modifier='auto' when option keys are {key_type}", + key_type=type(first_key), + ), ) else: # if its not the string 'auto' and its not a webgrid validator, assume @@ -464,8 +485,10 @@ def setup_validator(self): if not hasattr(self.value_modifier, 'process'): if not hasattr(self.value_modifier, '__call__'): raise TypeError( - _('value_modifier must be the string "auto", have a "process" attribute, ' - 'or be a callable') + _( + 'value_modifier must be the string "auto", have a "process" attribute, ' + 'or be a callable', + ), ) self.value_modifier = validators.CustomValidator(processor=self.value_modifier) @@ -486,7 +509,7 @@ def set(self, op, values, value2=None): if not op and not self.default_op: return self.op = op or self.default_op - self.using_default_op = (self.default_op is not None) + self.using_default_op = self.default_op is not None if self.using_default_op and op is None and self.default_value1 is not None: values = tolist(self._default_value(self.default_value1)) @@ -531,20 +554,23 @@ def process(self, value): def match_keys_for_value(self, value): """Used for single-search to match search value to part of an option's display string.""" return [ - key for (key, _) in filter( + key + for (key, _) in filter( lambda item: value.lower() in str(item[1]).lower(), - self.options_seq + self.options_seq, ) ] def get_search_expr(self): """Match up a search value to option display, grab the corresponding keys, and search.""" + # The important thing to remember here is that a user will be searching for the displayed # value, not the key that generated it. We need to do some prep work to search options # to get the keys needed for lookup into the data source. def search(value): matching_keys = self.match_keys_for_value(value) return self.sa_col.in_(matching_keys) + return search def apply(self, query): @@ -573,10 +599,23 @@ class OptionsIntFilterBase(OptionsFilterBase): as the `value_modifier`. """ - def __init__(self, sa_col, value_modifier=validators.IntValidator, default_op=None, - default_value1=None, default_value2=None): - OptionsFilterBase.__init__(self, sa_col, value_modifier, default_op, default_value1, - default_value2) + + def __init__( + self, + sa_col, + value_modifier=validators.IntValidator, + default_op=None, + default_value1=None, + default_value2=None, + ): + OptionsFilterBase.__init__( + self, + sa_col, + value_modifier, + default_op, + default_value1, + default_value2, + ) class OptionsEnumFilter(OptionsFilterBase): @@ -588,16 +627,17 @@ class OptionsEnumFilter(OptionsFilterBase): Notable args: enum_type (Enum): Python Enum type to use for options list. """ + enum_type = None def __init__( - self, - sa_col, - value_modifier=None, - default_op=None, - default_value1=None, - default_value2=None, - enum_type=None, + self, + sa_col, + value_modifier=None, + default_op=None, + default_value1=None, + default_value2=None, + enum_type=None, ): self.enum_type = enum_type or self.__class__.enum_type @@ -651,6 +691,7 @@ class OptionsEnumArrayFilter(OptionsEnumFilter): def get_search_expr(self): """Match up a search value to option display, grab the corresponding keys, and search.""" + # The important thing to remember here is that a user will be searching for the displayed # value, not the key that generated it. We need to do some prep work to search options # to get the keys needed for lookup into the data source. @@ -663,6 +704,7 @@ def search(value): if matching_keys: return self.sa_col.contains(matching_keys) return None + return search def apply(self, query): @@ -686,6 +728,7 @@ def apply(self, query): class TextFilter(FilterBase): """Filter with single freeform text input.""" + operators = (ops.eq, ops.not_eq, ops.contains, ops.not_contains, ops.empty, ops.not_empty) primary_op = ops.contains @@ -706,14 +749,14 @@ def comparisons(self): return { ops.eq: lambda col, value: sa.func.upper(col) == sa.func.upper(value), ops.not_eq: lambda col, value: sa.func.upper(col) != sa.func.upper(value), - ops.contains: lambda col, value: col.ilike(u'%{}%'.format(value)), - ops.not_contains: lambda col, value: ~col.ilike(u'%{}%'.format(value)) + ops.contains: lambda col, value: col.ilike(f'%{value}%'), + ops.not_contains: lambda col, value: ~col.ilike(f'%{value}%'), } return { ops.eq: lambda col, value: col == value, ops.not_eq: lambda col, value: col != value, - ops.contains: lambda col, value: col.like(u'%{}%'.format(value)), - ops.not_contains: lambda col, value: ~col.like(u'%{}%'.format(value)) + ops.contains: lambda col, value: col.like(f'%{value}%'), + ops.not_contains: lambda col, value: ~col.like(f'%{value}%'), } def get_search_expr(self): @@ -723,15 +766,19 @@ def apply(self, query): if self.op == self.default_op and not self.value1: return query if self.op == ops.empty: - return query.filter(or_( - self.sa_col.is_(None), - self.sa_col == u'', - )) + return query.filter( + or_( + self.sa_col.is_(None), + self.sa_col == '', + ), + ) if self.op == ops.not_empty: - return query.filter(and_( - self.sa_col.isnot(None), - self.sa_col != u'', - )) + return query.filter( + and_( + self.sa_col.isnot(None), + self.sa_col != '', + ), + ) if self.op in self.comparisons: return query.filter(self.comparisons[self.op](self.sa_col, self.value1)) @@ -744,14 +791,25 @@ class NumberFilterBase(FilterBase): Class attributes: validator (Validator): webgrid validator to use on input values. """ - operators = (ops.eq, ops.not_eq, ops.less_than_equal, ops.greater_than_equal, ops.between, - ops.not_between, ops.empty, ops.not_empty) + + operators = ( + ops.eq, + ops.not_eq, + ops.less_than_equal, + ops.greater_than_equal, + ops.between, + ops.not_between, + ops.empty, + ops.not_empty, + ) def process(self, value, is_value2): if self.op == self.default_op and not value: return None - if self.op in (ops.eq, ops.not_eq, ops.less_than_equal, - ops.greater_than_equal) and not is_value2: + if ( + self.op in (ops.eq, ops.not_eq, ops.less_than_equal, ops.greater_than_equal) + and not is_value2 + ): v_required = validators.RequiredValidator() return self.validator().process(v_required.process(value)) if value is None or value == '': @@ -763,7 +821,7 @@ def get_search_expr(self): # uses a LIKE. We could go nuts with things like stripping thousands separators, # parenthesis, monetary symbols, etc. from the search value, but then we get to deal with # locale. - return lambda value: sa.sql.cast(self.sa_col, sa.Unicode).like('%{}%'.format(value)) + return lambda value: sa.sql.cast(self.sa_col, sa.Unicode).like(f'%{value}%') def apply(self, query): filter_method = query.filter if not self.is_aggregate else query.having @@ -776,19 +834,22 @@ def apply(self, query): class IntFilter(NumberFilterBase): """Number filter validating inputs as integers.""" + validator = validators.IntValidator class AggregateIntFilter(IntFilter): """Number filter validating inputs as integers, for use on aggregate columns.""" + is_aggregate = True class NumberFilter(NumberFilterBase): """ - Same as int filter, but will handle real numbers and type - everything as a decimal.Decimal object + Same as int filter, but will handle real numbers and type + everything as a decimal.Decimal object """ + # our process() doesn't use a validator to return, but parent class does validator = validators.FloatValidator @@ -803,67 +864,88 @@ def process(self, value, is_value2): class AggregateNumberFilter(NumberFilter): """Number filter validating inputs as Decimal, for use on aggregate columns.""" + is_aggregate = True -class _DateMixin(object): +class _DateMixin: options_from = [ - (1, _('01-Jan')), (2, _('02-Feb')), (3, _('03-Mar')), (4, _('04-Apr')), - (5, _('05-May')), (6, _('06-Jun')), (7, _('07-Jul')), (8, _('08-Aug')), - (9, _('09-Sep')), (10, _('10-Oct')), (11, _('11-Nov')), (12, _('12-Dec')), + (1, _('01-Jan')), + (2, _('02-Feb')), + (3, _('03-Mar')), + (4, _('04-Apr')), + (5, _('05-May')), + (6, _('06-Jun')), + (7, _('07-Jul')), + (8, _('08-Aug')), + (9, _('09-Sep')), + (10, _('10-Oct')), + (11, _('11-Nov')), + (12, _('12-Dec')), ] - op_to_date_range = ImmutableDict({ - # these filters can be set as default ops without input values, so don't ignore them - ops.this_month: lambda self, today: ( - today + relativedelta(day=1), - today + relativedelta(day=1, months=+1, days=-1), - ), - ops.last_month: lambda self, today: ( - today + relativedelta(day=1, months=-1), - today + relativedelta(day=1, days=-1), - ), - ops.select_month: lambda self, today: self._select_month(today), - ops.this_year: lambda self, today: ( - dt.date(today.year, 1, 1), - dt.date(today.year, 12, 31), - ), - ops.this_week: lambda self, today: ( - today - relativedelta(weekday=SU(-1)), - today + relativedelta(weekday=calendar.SATURDAY), - ), - ops.last_week: lambda self, today: ( - today - relativedelta(weekday=SU(-1)) - relativedelta(days=7), - today + relativedelta(weekday=calendar.SATURDAY) - relativedelta(days=7), - ), - ops.today: lambda self, today: (today, today), - ops.in_past: lambda self, today: (today, today), - ops.in_future: lambda self, today: (today, today), - # ops with both dates populated - ops.between: lambda self, today: self._between_range(), - ops.not_between: lambda self, today: self._between_range(), - # ops with single date populated - ops.less_than_days_ago: lambda self, today: ( - today - dt.timedelta(days=self.value1), - today, - ) if self.value1 is not None else (None, None), - ops.in_less_than_days: lambda self, today: ( - today, - today + dt.timedelta(days=self.value1), - ) if self.value1 is not None else (None, None), - ops.days_ago: lambda self, today: ( - today - dt.timedelta(days=self.value1), - today - dt.timedelta(days=self.value1), - ) if self.value1 is not None else (None, None), - ops.more_than_days_ago: lambda self, today: ( - None, today - dt.timedelta(days=self.value1) - ) if self.value1 is not None else (None, None), - ops.in_days: lambda self, today: self._in_days(today), - ops.in_more_than_days: lambda self, today: self._in_days(today), - ops.eq: lambda self, today: self._equality(), - ops.not_eq: lambda self, today: self._equality(), - ops.less_than_equal: lambda self, today: self._equality(), - ops.greater_than_equal: lambda self, today: self._equality(), - }) + op_to_date_range = ImmutableDict( + { + # these filters can be set as default ops without input values, so don't ignore them + ops.this_month: lambda self, today: ( + today + relativedelta(day=1), + today + relativedelta(day=1, months=+1, days=-1), + ), + ops.last_month: lambda self, today: ( + today + relativedelta(day=1, months=-1), + today + relativedelta(day=1, days=-1), + ), + ops.select_month: lambda self, today: self._select_month(today), + ops.this_year: lambda self, today: ( + dt.date(today.year, 1, 1), + dt.date(today.year, 12, 31), + ), + ops.this_week: lambda self, today: ( + today - relativedelta(weekday=SU(-1)), + today + relativedelta(weekday=calendar.SATURDAY), + ), + ops.last_week: lambda self, today: ( + today - relativedelta(weekday=SU(-1)) - relativedelta(days=7), + today + relativedelta(weekday=calendar.SATURDAY) - relativedelta(days=7), + ), + ops.today: lambda self, today: (today, today), + ops.in_past: lambda self, today: (today, today), + ops.in_future: lambda self, today: (today, today), + # ops with both dates populated + ops.between: lambda self, today: self._between_range(), + ops.not_between: lambda self, today: self._between_range(), + # ops with single date populated + ops.less_than_days_ago: lambda self, today: ( + today - dt.timedelta(days=self.value1), + today, + ) + if self.value1 is not None + else (None, None), + ops.in_less_than_days: lambda self, today: ( + today, + today + dt.timedelta(days=self.value1), + ) + if self.value1 is not None + else (None, None), + ops.days_ago: lambda self, today: ( + today - dt.timedelta(days=self.value1), + today - dt.timedelta(days=self.value1), + ) + if self.value1 is not None + else (None, None), + ops.more_than_days_ago: lambda self, today: ( + None, + today - dt.timedelta(days=self.value1), + ) + if self.value1 is not None + else (None, None), + ops.in_days: lambda self, today: self._in_days(today), + ops.in_more_than_days: lambda self, today: self._in_days(today), + ops.eq: lambda self, today: self._equality(), + ops.not_eq: lambda self, today: self._equality(), + ops.less_than_equal: lambda self, today: self._equality(), + ops.greater_than_equal: lambda self, today: self._equality(), + }, + ) @property def options_seq(self): @@ -879,14 +961,11 @@ def format_display_vals(self): ops.less_than_equal.key, ops.greater_than_equal.key, ops.between.key, - ops.not_between.key + ops.not_between.key, ): # !!!: localize self.value1_set_with = self.value1.strftime('%Y-%m-%d') - if isinstance(self.value2, dt.date) and self.op in ( - ops.between.key, - ops.not_between.key - ): + if isinstance(self.value2, dt.date) and self.op in (ops.between.key, ops.not_between.key): # !!!: localize self.value2_set_with = self.value2.strftime('%Y-%m-%d') @@ -920,9 +999,10 @@ def _select_month(self, today): def _description_data(self): today = self._get_today() - first_day, last_day = self.op_to_date_range.get( - self.op, lambda self, today: (None, None) - )(self, today) + first_day, last_day = self.op_to_date_range.get(self.op, lambda self, today: (None, None))( + self, + today, + ) prefix = { ops.more_than_days_ago: _('before '), @@ -940,17 +1020,15 @@ def _description_data(self): @property def description(self): """ - String description of the filter operation and values - - Useful for Excel reports + String description of the filter operation and values + - Useful for Excel reports """ # simple cases if self.error: return _('invalid') elif self.op == ops.select_month: - if not ( - isinstance(self.value1, int) and isinstance(self.value2, int) - ): + if not (isinstance(self.value1, int) and isinstance(self.value2, int)): return _('All') if self.value1 < 1 or self.value1 > 12: return self.value2 @@ -964,34 +1042,35 @@ def description(self): first_day, last_day, prefix = self._description_data() if not first_day and ( - not self.op or ( - self.default_op == self.op and ( - self.value2 is None and self.value1 is None - ) - ) + not self.op + or (self.default_op == self.op and (self.value2 is None and self.value1 is None)) ): return _('all') if self.op in ( - ops.today, ops.eq, ops.not_eq, ops.less_than_equal, - ops.greater_than_equal, ops.days_ago, ops.in_past, ops.in_future, + ops.today, + ops.eq, + ops.not_eq, + ops.less_than_equal, + ops.greater_than_equal, + ops.days_ago, + ops.in_past, + ops.in_future, ): # !!!: localize - return _('{descriptor}{date}', - descriptor=prefix, - date=first_day.strftime('%m/%d/%Y')) + return _('{descriptor}{date}', descriptor=prefix, date=first_day.strftime('%m/%d/%Y')) elif last_day and first_day: # !!!: localize dates - return _('{descriptor}{first_date} - {second_date}', - descriptor=prefix, - first_date=first_day.strftime('%m/%d/%Y'), - second_date=last_day.strftime('%m/%d/%Y')) + return _( + '{descriptor}{first_date} - {second_date}', + descriptor=prefix, + first_date=first_day.strftime('%m/%d/%Y'), + second_date=last_day.strftime('%m/%d/%Y'), + ) else: # !!!: localize target_date = first_day if first_day else last_day - return _('{descriptor}{date}', - descriptor=prefix, - date=target_date.strftime('%m/%d/%Y')) + return _('{descriptor}{date}', descriptor=prefix, date=target_date.strftime('%m/%d/%Y')) def valid_date_for_backend(self, value): """ @@ -1035,18 +1114,16 @@ def get_search_expr(self, date_comparator=None): date_comparator = date_comparator or (lambda value: self.sa_col == value) def expr(value): - base_expr = sa.sql.cast(self.sa_col, sa.Unicode).like('%{}%'.format(value)) + base_expr = sa.sql.cast(self.sa_col, sa.Unicode).like(f'%{value}%') try: date_value = parse(value) if not self.valid_date_for_backend(date_value): return base_expr - return or_( - base_expr, - date_comparator(date_value) - ) + return or_(base_expr, date_comparator(date_value)) except (ValueError, OverflowError): pass return base_expr + return expr def serialize_filter_spec(self): @@ -1054,56 +1131,62 @@ def serialize_filter_spec(self): return types.OptionsFilterSpec( operators=base_spec.operators, primary_op=base_spec.primary_op, - options=[ - self.serialize_filter_option(key, value) - for key, value in self.options_seq], + options=[self.serialize_filter_option(key, value) for key, value in self.options_seq], ) class _DateOpQueryMixin: - op_to_query = ImmutableDict({ - ops.today: lambda self, query, today: query.filter( - self.sa_col == today - ), - ops.in_past: lambda self, query, today: query.filter(self.sa_col < today), - ops.in_future: lambda self, query, today: query.filter(self.sa_col > today), - ops.this_week: lambda self, query, today: query.filter(self.sa_col.between( - today - relativedelta(weekday=SU(-1)), - today + relativedelta(weekday=calendar.SATURDAY), - )), - ops.last_week: lambda self, query, today: query.filter(self.sa_col.between( - today - relativedelta(weekday=SU(-1)) - relativedelta(days=7), - today + relativedelta(weekday=calendar.SATURDAY) - relativedelta(days=7), - )), - ops.select_month: lambda self, query, today: ( - self._month_year_filter(query) if self.value1 and self.value2 else query - ), - ops.this_month: lambda self, query, today: self._month_year_filter(query), - ops.last_month: lambda self, query, today: self._month_year_filter(query), - ops.this_year: lambda self, query, today: self._month_year_filter(query), - ops.between: lambda self, query, today: query.filter(self._between_clause()), - ops.not_between: lambda self, query, today: query.filter(~self._between_clause()), - ops.days_ago: lambda self, query, today: query.filter( - self.sa_col == today - dt.timedelta(days=self.value1) - ), - ops.less_than_days_ago: lambda self, query, today: query.filter(and_( - self.sa_col > today - dt.timedelta(days=self.value1), - self.sa_col <= today, - )), - ops.more_than_days_ago: lambda self, query, today: query.filter( - self.sa_col < today - dt.timedelta(days=self.value1) - ), - ops.in_days: lambda self, query, today: query.filter( - self.sa_col == today + dt.timedelta(days=self.value1) - ), - ops.in_less_than_days: lambda self, query, today: query.filter(and_( - self.sa_col >= today, - self.sa_col < today + dt.timedelta(days=self.value1), - )), - ops.in_more_than_days: lambda self, query, today: query.filter( - self.sa_col > today + dt.timedelta(days=self.value1) - ), - }) + op_to_query = ImmutableDict( + { + ops.today: lambda self, query, today: query.filter(self.sa_col == today), + ops.in_past: lambda self, query, today: query.filter(self.sa_col < today), + ops.in_future: lambda self, query, today: query.filter(self.sa_col > today), + ops.this_week: lambda self, query, today: query.filter( + self.sa_col.between( + today - relativedelta(weekday=SU(-1)), + today + relativedelta(weekday=calendar.SATURDAY), + ), + ), + ops.last_week: lambda self, query, today: query.filter( + self.sa_col.between( + today - relativedelta(weekday=SU(-1)) - relativedelta(days=7), + today + relativedelta(weekday=calendar.SATURDAY) - relativedelta(days=7), + ), + ), + ops.select_month: lambda self, query, today: ( + self._month_year_filter(query) if self.value1 and self.value2 else query + ), + ops.this_month: lambda self, query, today: self._month_year_filter(query), + ops.last_month: lambda self, query, today: self._month_year_filter(query), + ops.this_year: lambda self, query, today: self._month_year_filter(query), + ops.between: lambda self, query, today: query.filter(self._between_clause()), + ops.not_between: lambda self, query, today: query.filter(~self._between_clause()), + ops.days_ago: lambda self, query, today: query.filter( + self.sa_col == today - dt.timedelta(days=self.value1), + ), + ops.less_than_days_ago: lambda self, query, today: query.filter( + and_( + self.sa_col > today - dt.timedelta(days=self.value1), + self.sa_col <= today, + ), + ), + ops.more_than_days_ago: lambda self, query, today: query.filter( + self.sa_col < today - dt.timedelta(days=self.value1), + ), + ops.in_days: lambda self, query, today: query.filter( + self.sa_col == today + dt.timedelta(days=self.value1), + ), + ops.in_less_than_days: lambda self, query, today: query.filter( + and_( + self.sa_col >= today, + self.sa_col < today + dt.timedelta(days=self.value1), + ), + ), + ops.in_more_than_days: lambda self, query, today: query.filter( + self.sa_col > today + dt.timedelta(days=self.value1), + ), + }, + ) def _get_today(self): # this filter is date-only, so our "now" is a date without time @@ -1131,21 +1214,51 @@ class DateFilter(_DateOpQueryMixin, _DateMixin, FilterBase): _now (datetime, optional): Useful for testing, supplies a date the filter will use instead of the true `datetime.now()`. Defaults to None. """ + operators = ( - ops.eq, ops.not_eq, ops.in_past, ops.in_future, ops.less_than_equal, - ops.greater_than_equal, ops.between, ops.not_between, - ops.days_ago, ops.less_than_days_ago, ops.more_than_days_ago, - ops.today, ops.this_week, ops.last_week, ops.in_days, ops.in_less_than_days, - ops.in_more_than_days, ops.empty, ops.not_empty, ops.this_month, - ops.last_month, ops.select_month, ops.this_year + ops.eq, + ops.not_eq, + ops.in_past, + ops.in_future, + ops.less_than_equal, + ops.greater_than_equal, + ops.between, + ops.not_between, + ops.days_ago, + ops.less_than_days_ago, + ops.more_than_days_ago, + ops.today, + ops.this_week, + ops.last_week, + ops.in_days, + ops.in_less_than_days, + ops.in_more_than_days, + ops.empty, + ops.not_empty, + ops.this_month, + ops.last_month, + ops.select_month, + ops.this_year, ) days_operators = ( - ops.days_ago, ops.less_than_days_ago, ops.more_than_days_ago, - ops.in_less_than_days, ops.in_more_than_days, ops.in_days + ops.days_ago, + ops.less_than_days_ago, + ops.more_than_days_ago, + ops.in_less_than_days, + ops.in_more_than_days, + ops.in_days, ) no_value_operators = ( - ops.empty, ops.not_empty, ops.today, ops.this_week, ops.last_week, ops.this_month, - ops.last_month, ops.this_year, ops.in_past, ops.in_future, + ops.empty, + ops.not_empty, + ops.today, + ops.this_week, + ops.last_week, + ops.this_month, + ops.last_month, + ops.this_year, + ops.in_past, + ops.in_future, ) input_types = 'input', 'select', 'input2' html_input_types = { @@ -1157,12 +1270,23 @@ class DateFilter(_DateOpQueryMixin, _DateMixin, FilterBase): ops.not_between: 'date', } - def __init__(self, sa_col, _now=None, default_op=None, default_value1=None, - default_value2=None): + def __init__( + self, + sa_col, + _now=None, + default_op=None, + default_value1=None, + default_value2=None, + ): self.first_day = None self.last_day = None - FilterBase.__init__(self, sa_col, default_op=default_op, default_value1=default_value1, - default_value2=default_value2) + FilterBase.__init__( + self, + sa_col, + default_op=default_op, + default_value1=default_value1, + default_value2=default_value2, + ) # attributes from static instance self._now = _now @@ -1185,7 +1309,8 @@ def set(self, op, value1, value2=None): # store first/last day for customized usage today = self._get_today() self.first_day, self.last_day = self.op_to_date_range.get( - self.op, lambda self, today: (None, None) + self.op, + lambda self, today: (None, None), )(self, today) def apply(self, query): @@ -1200,8 +1325,15 @@ def apply(self, query): filtered_query = super().apply(query) if self.op in ( - ops.today, ops.this_week, ops.last_week, ops.select_month, ops.this_month, - ops.last_month, ops.this_year, ops.in_past, ops.in_future, + ops.today, + ops.this_week, + ops.last_week, + ops.select_month, + ops.this_month, + ops.last_month, + ops.this_year, + ops.in_past, + ops.in_future, ): return filtered_query @@ -1225,15 +1357,21 @@ def _process_days_operator(self, value, is_value2): try: self._get_today() - dt.timedelta(days=filter_value) except OverflowError: - raise validators.ValueInvalid(gettext('date filter given is out of range'), - value, self) + raise validators.ValueInvalid( + gettext('date filter given is out of range'), + value, + self, + ) if self.op in (ops.in_days, ops.in_less_than_days, ops.in_more_than_days): try: self._get_today() + dt.timedelta(days=filter_value) except OverflowError: - raise validators.ValueInvalid(gettext('date filter given is out of range'), - value, self) + raise validators.ValueInvalid( + gettext('date filter given is out of range'), + value, + self, + ) return filter_value @@ -1295,6 +1433,7 @@ class DateTimeFilter(DateFilter): _now (datetime, optional): Useful for testing, supplies a date the filter will use instead of the true `datetime.now()`. Defaults to None. """ + html_input_types = { ops.eq: 'datetime-local', ops.not_eq: 'datetime-local', @@ -1303,105 +1442,145 @@ class DateTimeFilter(DateFilter): ops.between: 'datetime-local', ops.not_between: 'datetime-local', } - op_to_query = ImmutableDict({**DateFilter.op_to_query, **{ - ops.today: lambda self, query, today: query.filter(self.sa_col.between( - ensure_datetime(today), - ensure_datetime(today, time_part=dt.time(23, 59, 59, 999999)), - )), - ops.in_past: lambda self, query, today: query.filter( - self.sa_col < ensure_datetime(today), - ), - ops.in_future: lambda self, query, today: query.filter( - self.sa_col > ensure_datetime(today, time_part=dt.time(23, 59, 59, 999999)), - ), - ops.this_week: lambda self, query, today: query.filter(self.sa_col.between( - ensure_datetime(today - relativedelta(weekday=SU(-1))), - ensure_datetime( - today + relativedelta(weekday=calendar.SATURDAY), - time_part=dt.time(23, 59, 59, 999999) + op_to_query = ImmutableDict( + { + **DateFilter.op_to_query, + ops.today: lambda self, query, today: query.filter( + self.sa_col.between( + ensure_datetime(today), + ensure_datetime(today, time_part=dt.time(23, 59, 59, 999999)), + ), ), - )), - ops.last_week: lambda self, query, today: query.filter(self.sa_col.between( - ensure_datetime(today - relativedelta(weekday=SU(-1)) - relativedelta(days=7)), - ensure_datetime( - today + relativedelta(weekday=calendar.SATURDAY) - relativedelta(days=7), - time_part=dt.time(23, 59, 59, 999999) + ops.in_past: lambda self, query, today: query.filter( + self.sa_col < ensure_datetime(today), ), - )), - ops.this_month: lambda self, query, today: query.filter(self.sa_col.between( - ensure_datetime(today + relativedelta(day=1)), - today + relativedelta(day=1, months=+1, microseconds=-1), - )), - ops.last_month: lambda self, query, today: query.filter(self.sa_col.between( - ensure_datetime(today + relativedelta(day=1, months=-1)), - today + relativedelta(day=1, microseconds=-1), - )), - ops.this_year: lambda self, query, today: query.filter(self.sa_col.between( - ensure_datetime(today + relativedelta(day=1, month=1)), - today + relativedelta(day=31, month=12, days=1, microseconds=-1), - )), - ops.select_month: lambda self, query, today: ( - query.filter(self.sa_col.between( - ensure_datetime(self.first_day), - self.last_day + relativedelta(days=1, microseconds=-1), - )) if self.value2 else query - ), - ops.days_ago: lambda self, query, today: query.filter(self.sa_col.between( - ensure_datetime(today - dt.timedelta(days=self.value1)), - ensure_datetime( - today - dt.timedelta(days=self.value1), - time_part=dt.time(23, 59, 59, 999999) + ops.in_future: lambda self, query, today: query.filter( + self.sa_col > ensure_datetime(today, time_part=dt.time(23, 59, 59, 999999)), ), - )), - ops.less_than_days_ago: lambda self, query, today: query.filter(and_( - self.sa_col > ensure_datetime(today - dt.timedelta(days=self.value1), - time_part=dt.time(23, 59, 59, 999999)), - self.sa_col <= self._get_now() - )), - ops.more_than_days_ago: lambda self, query, today: query.filter( - self.sa_col < ensure_datetime(today - dt.timedelta(days=self.value1)) - ), - ops.in_days: lambda self, query, today: query.filter(self.sa_col.between( - ensure_datetime(today + dt.timedelta(days=self.value1)), - ensure_datetime( - today + dt.timedelta(days=self.value1), time_part=dt.time(23, 59, 59, 999999) + ops.this_week: lambda self, query, today: query.filter( + self.sa_col.between( + ensure_datetime(today - relativedelta(weekday=SU(-1))), + ensure_datetime( + today + relativedelta(weekday=calendar.SATURDAY), + time_part=dt.time(23, 59, 59, 999999), + ), + ), ), - )), - ops.in_less_than_days: lambda self, query, today: query.filter(and_( - self.sa_col >= self._get_now(), - self.sa_col < ensure_datetime(today + dt.timedelta(days=self.value1)), - )), - ops.in_more_than_days: lambda self, query, today: query.filter( - self.sa_col > ensure_datetime( - today + dt.timedelta(days=self.value1), - time_part=dt.time(23, 59, 59, 999999) - ) - ), - ops.eq: lambda self, query, today: ( - query.filter(self._eq_clause()) - ), - ops.not_eq: lambda self, query, today: ( - query.filter(~self._eq_clause()) - ), - ops.less_than_equal: lambda self, query, today: query.filter( - self.sa_col <= ensure_datetime( - self.value1.date(), time_part=dt.time(23, 59, 59, 999999) + ops.last_week: lambda self, query, today: query.filter( + self.sa_col.between( + ensure_datetime( + today - relativedelta(weekday=SU(-1)) - relativedelta(days=7), + ), + ensure_datetime( + today + relativedelta(weekday=calendar.SATURDAY) - relativedelta(days=7), + time_part=dt.time(23, 59, 59, 999999), + ), + ), + ), + ops.this_month: lambda self, query, today: query.filter( + self.sa_col.between( + ensure_datetime(today + relativedelta(day=1)), + today + relativedelta(day=1, months=+1, microseconds=-1), + ), + ), + ops.last_month: lambda self, query, today: query.filter( + self.sa_col.between( + ensure_datetime(today + relativedelta(day=1, months=-1)), + today + relativedelta(day=1, microseconds=-1), + ), + ), + ops.this_year: lambda self, query, today: query.filter( + self.sa_col.between( + ensure_datetime(today + relativedelta(day=1, month=1)), + today + relativedelta(day=31, month=12, days=1, microseconds=-1), + ), + ), + ops.select_month: lambda self, query, today: ( + query.filter( + self.sa_col.between( + ensure_datetime(self.first_day), + self.last_day + relativedelta(days=1, microseconds=-1), + ), + ) + if self.value2 + else query + ), + ops.days_ago: lambda self, query, today: query.filter( + self.sa_col.between( + ensure_datetime(today - dt.timedelta(days=self.value1)), + ensure_datetime( + today - dt.timedelta(days=self.value1), + time_part=dt.time(23, 59, 59, 999999), + ), + ), + ), + ops.less_than_days_ago: lambda self, query, today: query.filter( + and_( + self.sa_col + > ensure_datetime( + today - dt.timedelta(days=self.value1), + time_part=dt.time(23, 59, 59, 999999), + ), + self.sa_col <= self._get_now(), + ), + ), + ops.more_than_days_ago: lambda self, query, today: query.filter( + self.sa_col < ensure_datetime(today - dt.timedelta(days=self.value1)), + ), + ops.in_days: lambda self, query, today: query.filter( + self.sa_col.between( + ensure_datetime(today + dt.timedelta(days=self.value1)), + ensure_datetime( + today + dt.timedelta(days=self.value1), + time_part=dt.time(23, 59, 59, 999999), + ), + ), + ), + ops.in_less_than_days: lambda self, query, today: query.filter( + and_( + self.sa_col >= self._get_now(), + self.sa_col < ensure_datetime(today + dt.timedelta(days=self.value1)), + ), + ), + ops.in_more_than_days: lambda self, query, today: query.filter( + self.sa_col + > ensure_datetime( + today + dt.timedelta(days=self.value1), + time_part=dt.time(23, 59, 59, 999999), + ), + ), + ops.eq: lambda self, query, today: (query.filter(self._eq_clause())), + ops.not_eq: lambda self, query, today: (query.filter(~self._eq_clause())), + ops.less_than_equal: lambda self, query, today: query.filter( + self.sa_col + <= ensure_datetime(self.value1.date(), time_part=dt.time(23, 59, 59, 999999)), ) - ) if self._has_date_only1 else None, - ops.between: lambda self, query, today: query.filter(self._between_clause()), - ops.not_between: lambda self, query, today: query.filter(~self._between_clause()), - }}) + if self._has_date_only1 + else None, + ops.between: lambda self, query, today: query.filter(self._between_clause()), + ops.not_between: lambda self, query, today: query.filter(~self._between_clause()), + }, + ) - def __init__(self, sa_col, _now=None, default_op=None, default_value1=None, - default_value2=None): + def __init__( + self, + sa_col, + _now=None, + default_op=None, + default_value1=None, + default_value2=None, + ): self._has_date_only1 = self._has_date_only2 = False - super(DateTimeFilter, self).__init__(sa_col, _now=_now, default_op=default_op, - default_value1=default_value1, - default_value2=default_value2) + super(DateTimeFilter, self).__init__( + sa_col, + _now=_now, + default_op=default_op, + default_value1=default_value1, + default_value2=default_value2, + ) def check_arrow_type(self): """DateTimeFilter has no problems with ArrowType. Pass this case through.""" - pass def format_display_vals(self): ops_single_val = ( @@ -1410,10 +1589,7 @@ def format_display_vals(self): ops.less_than_equal.key, ops.greater_than_equal.key, ) - ops_double_val = ( - ops.between.key, - ops.not_between.key - ) + ops_double_val = (ops.between.key, ops.not_between.key) if isinstance(self.value1, dt.datetime) and self.op in ops_single_val + ops_double_val: # !!!: localize self.value1_set_with = self.value1.strftime('%Y-%m-%dT%H:%M') @@ -1429,8 +1605,7 @@ def format_display_vals(self): def _between_clause(self): if self._has_date_only2: - right_side = ensure_datetime(self.value2.date(), - time_part=dt.time(23, 59, 59, 999999)) + right_side = ensure_datetime(self.value2.date(), time_part=dt.time(23, 59, 59, 999999)) elif self.value2.second == 0 and self.value2.microsecond == 0: right_side = self.value2 + dt.timedelta(seconds=59, microseconds=999999) else: @@ -1505,7 +1680,7 @@ def _has_date_only(self, dt_value, value): dt_value.hour == 0 and dt_value.minute == 0 and dt_value.second == 0 - and '00:00' not in value + and '00:00' not in value, ) def get_search_expr(self): @@ -1524,8 +1699,17 @@ def date_comparator(value): class TimeFilter(FilterBase): """Time filter with one or two freeform inputs.""" - operators = (ops.eq, ops.not_eq, ops.less_than_equal, ops.greater_than_equal, ops.between, - ops.not_between, ops.empty, ops.not_empty) + + operators = ( + ops.eq, + ops.not_eq, + ops.less_than_equal, + ops.greater_than_equal, + ops.between, + ops.not_between, + ops.empty, + ops.not_empty, + ) input_types = 'input', 'input2' html_input_types = { ops.eq: 'time', @@ -1560,7 +1744,7 @@ def apply(self, query): dt.datetime.combine(dt.date.today(), self.value1) + dt.timedelta(seconds=59, microseconds=999999) ).time(), - sa.Time + sa.Time, ) if self.op == ops.eq: @@ -1590,7 +1774,7 @@ def process(self, value, is_value2): def get_search_expr(self, date_comparator=None): # This is a naive implementation that simply converts the time column to string and # uses a LIKE. - return lambda value: sa.sql.cast(self.sa_col, sa.Unicode).like('%{}%'.format(value)) + return lambda value: sa.sql.cast(self.sa_col, sa.Unicode).like(f'%{value}%') class YesNoFilter(FilterBase): @@ -1598,16 +1782,13 @@ class YesNoFilter(FilterBase): No inputs, just the all/yes/no operators. """ - class ops(object): + + class ops: all = Operator('a', _('all'), None) yes = Operator('y', _('yes'), None) no = Operator('n', _('no'), None) - operators = ( - ops.all, - ops.yes, - ops.no - ) + operators = (ops.all, ops.yes, ops.no) primary_op = ops.yes def get_search_expr(self): @@ -1617,6 +1798,7 @@ def expr(value): elif value.lower() in self.ops.no.display: return self.sa_col == sa.false() return None + return expr def apply(self, query): diff --git a/src/webgrid/flask.py b/src/webgrid/flask.py index d039a09..5c582e2 100644 --- a/src/webgrid/flask.py +++ b/src/webgrid/flask.py @@ -1,14 +1,14 @@ -from __future__ import absolute_import import json import flask from webgrid import extensions, renderers + try: from morphi.helpers.jinja import configure_jinja_environment except ImportError: - configure_jinja_environment = lambda *args, **kwargs: None # noqa: E731 + configure_jinja_environment = lambda *args, **kwargs: None class WebGrid(extensions.FrameworkManager): @@ -38,16 +38,28 @@ class MyGrid(BaseGrid): Default "webgrid". Needs to be unique if multiple managers are initialized as flask extensions. """ + blueprint_name = 'webgrid' blueprint_class = flask.Blueprint - def __init__(self, db=None, jinja_loader=None, args_loaders=None, session_max_hours=None, - blueprint_name=None, blueprint_class=None): + def __init__( + self, + db=None, + jinja_loader=None, + args_loaders=None, + session_max_hours=None, + blueprint_name=None, + blueprint_class=None, + ): self.blueprint_name = blueprint_name or self.blueprint_name self.blueprint_class = blueprint_class or self.blueprint_class self._registered_grids = {} - super().__init__(db=db, jinja_loader=jinja_loader, args_loaders=args_loaders, - session_max_hours=session_max_hours) + super().__init__( + db=db, + jinja_loader=jinja_loader, + args_loaders=args_loaders, + session_max_hours=session_max_hours, + ) def init_db(self, db): """Set the db connector.""" @@ -72,6 +84,7 @@ def request_url_args(self): def csrf_token(self): """Return a CSRF token for POST.""" from flask_wtf.csrf import generate_csrf + return generate_csrf() def web_session(self): @@ -96,7 +109,7 @@ def test_request_context(self, url='/'): def static_url(self, url_tail): """Construct static URL from webgrid blueprint.""" - return flask.url_for('{}.static'.format(self.blueprint_name), filename=url_tail) + return flask.url_for(f'{self.blueprint_name}.static', filename=url_tail) def init_blueprint(self, app): """Create a blueprint for webgrid assets.""" @@ -104,7 +117,7 @@ def init_blueprint(self, app): self.blueprint_name, __name__, static_folder='static', - static_url_path=app.static_url_path + '/webgrid' + static_url_path=app.static_url_path + '/webgrid', ) def init_app(self, app): @@ -115,9 +128,13 @@ def init_app(self, app): def file_as_response(self, data_stream, file_name, mime_type): """Return response from framework for sending a file.""" - as_attachment = (file_name is not None) - return flask.send_file(data_stream, mimetype=mime_type, as_attachment=as_attachment, - download_name=file_name) + as_attachment = file_name is not None + return flask.send_file( + data_stream, + mimetype=mime_type, + as_attachment=as_attachment, + download_name=file_name, + ) class WebGridAPI(WebGrid): @@ -147,18 +164,32 @@ class attribute and apply ``CSRFProtect`` to your application. Note that in that Default "/webgrid-api". By default, ``api_route`` uses this to construct "/webgrid-api/". """ + blueprint_name = 'webgrid-api' api_route_prefix = '/webgrid-api' - args_loaders = (extensions.RequestJsonLoader, ) + args_loaders = (extensions.RequestJsonLoader,) csrf_protection = False - def __init__(self, db=None, jinja_loader=None, args_loaders=None, session_max_hours=None, - blueprint_name=None, blueprint_class=None, api_route_prefix=None): + def __init__( + self, + db=None, + jinja_loader=None, + args_loaders=None, + session_max_hours=None, + blueprint_name=None, + blueprint_class=None, + api_route_prefix=None, + ): self.api_route_prefix = api_route_prefix or self.api_route_prefix self._registered_grids = {} - super().__init__(db=db, jinja_loader=jinja_loader, args_loaders=args_loaders, - session_max_hours=session_max_hours, blueprint_name=blueprint_name, - blueprint_class=blueprint_class) + super().__init__( + db=db, + jinja_loader=jinja_loader, + args_loaders=args_loaders, + session_max_hours=session_max_hours, + blueprint_name=blueprint_name, + blueprint_class=blueprint_class, + ) def init_blueprint(self, app): """Create a blueprint for webgrid assets and set up a generic API endpoint.""" @@ -168,17 +199,15 @@ def init_blueprint(self, app): if not self.csrf_protection and app.extensions.get('csrf'): app.extensions['csrf'].exempt(blueprint) - blueprint.route(self.api_route, methods=('POST', ))( - self.api_view_method - ) - blueprint.route(self.api_route + '/count', methods=('POST', ))( - self.api_count_view_method - ) + blueprint.route(self.api_route, methods=('POST',))(self.api_view_method) + blueprint.route(self.api_route + '/count', methods=('POST',))(self.api_count_view_method) if app.config.get('TESTING'): - @blueprint.route(self.api_route_prefix + '/testing/__csrf__', methods=('GET', )) + + @blueprint.route(self.api_route_prefix + '/testing/__csrf__', methods=('GET',)) def csrf_get(): from flask_wtf.csrf import generate_csrf + return generate_csrf() return blueprint @@ -208,7 +237,6 @@ def api_init_grid(self, grid_cls_or_creator): def api_init_grid_post(self, grid): """Hook to run API-level init on every grid after instantiation""" - pass def api_on_render_limit_exceeded(self, grid): """Export failed due to number of records. Returns a JSON response.""" diff --git a/src/webgrid/renderers.py b/src/webgrid/renderers.py index ce5597e..6cf9596 100644 --- a/src/webgrid/renderers.py +++ b/src/webgrid/renderers.py @@ -1,42 +1,35 @@ -from __future__ import absolute_import - -import re from abc import ABC, abstractmethod +from collections import defaultdict +import csv from dataclasses import asdict import inspect import io -from operator import itemgetter -from collections import defaultdict import json -from typing import Dict, Union +from operator import itemgetter +import re import typing - -import six -from blazeutils.functional import identity -from markupsafe import Markup +from typing import Dict, Union from blazeutils.containers import HTMLAttributes, LazyDict +from blazeutils.functional import identity from blazeutils.helpers import tolist from blazeutils.jsonh import jsonmod -from blazeutils.spreadsheets import WriterX, xlsxwriter, openpyxl -from blazeutils.strings import reindent, randnumerics +from blazeutils.spreadsheets import WriterX, openpyxl, xlsxwriter +from blazeutils.strings import randnumerics, reindent import jinja2 as jinja - +from markupsafe import Markup +import six from werkzeug.datastructures import MultiDict from werkzeug.routing import Map, Rule -from .extensions import ( - CustomJsonEncoder, - gettext as _, - ngettext, - translation_manager -) -from .utils import current_url from . import extensions, types -import csv +from .extensions import CustomJsonEncoder, ngettext, translation_manager +from .extensions import gettext as _ +from .utils import current_url + if openpyxl: - from openpyxl.styles import Font, Border, Side, Alignment + from openpyxl.styles import Alignment, Border, Font, Side from openpyxl.utils import get_column_letter else: Font = Border = Side = Alignment = typing.Any @@ -44,12 +37,12 @@ try: from morphi.helpers.jinja import configure_jinja_environment except ImportError: - configure_jinja_environment = lambda *args, **kwargs: None # noqa: E731 + configure_jinja_environment = lambda *args, **kwargs: None try: from speaklater import is_lazy_string except ImportError: - is_lazy_string = lambda value: False # noqa: E731 + is_lazy_string = lambda value: False def fix_xls_value(value): @@ -80,13 +73,13 @@ class Renderer(ABC): Args: grid (BaseGrid): Parent grid of this renderer instance. """ + _columns = None @property @abstractmethod def name(self): """Identifier used to find columns that will render on this target.""" - pass @property def columns(self): @@ -118,7 +111,6 @@ def can_render(self): @abstractmethod def render(self): """Main renderer method returning the output.""" - pass class GroupMixin: @@ -172,9 +164,9 @@ def _safe_id(idstring): TODO: Set IDs explicitly and don't rely on this being applied to name attributes """ # Transform all whitespace to underscore - idstring = re.sub(r'\s', "_", '%s' % idstring) + idstring = re.sub(r'\s', '_', '%s' % idstring) # Remove everything that is not a hyphen or a member of \w - idstring = re.sub(r'(?!-)\W', "", idstring).lower() + idstring = re.sub(r'(?!-)\W', '', idstring).lower() return idstring @@ -188,7 +180,7 @@ def render_attr(key, value): return Markup.escape(key) elif value is False or value is None: return Markup('') - return Markup('{}="{}"'.format(Markup.escape(key), Markup.escape(value))) + return Markup(f'{Markup.escape(key)}="{Markup.escape(value)}"') attrs = sorted(attrs.items(), key=itemgetter(0)) rendered_attrs = filter(identity, (render_attr(k, v) for k, v in attrs)) @@ -197,6 +189,7 @@ def render_attr(key, value): class JSON(Renderer): """Renderer for JSON output""" + mime_type = 'application/json' @property @@ -220,10 +213,7 @@ def serialize_column_group(self, label, columns): def serialized_column_groups(self): group_to_keys = defaultdict(list) - for column in filter( - lambda col: col.group is not None, - self.columns - ): + for column in filter(lambda col: col.group is not None, self.columns): group_to_keys[column.group].append(column.key) return [ self.serialize_column_group(group.label, columns) @@ -339,7 +329,7 @@ def init(self): self.jinja_env = jinja.Environment( loader=jinja.PackageLoader('webgrid', 'templates'), finalize=lambda x: x if x is not None else '', - autoescape=True + autoescape=True, ) self.jinja_env.filters['wg_safe'] = jinja.filters.do_mark_safe self.jinja_env.filters['wg_attributes'] = render_html_attributes @@ -371,11 +361,7 @@ def header(self): def header_form_attrs(self, **kwargs): """HTML attributes to render on the grid header form element.""" - return { - 'method': self.form_action_method(), - 'action': self.form_action_url(), - **kwargs - } + return {'method': self.form_action_method(), 'action': self.form_action_url(), **kwargs} def form_action_method(self): """Detect whether the header form should have a GET or POST action. @@ -405,7 +391,7 @@ def filtering_session_key(self): """Hidden input to preserve the session key on form submission.""" return self._render_jinja( '', - value=self.grid.session_key + value=self.grid.session_key, ) def filtering_fields(self): @@ -427,7 +413,7 @@ def filtering_table_row(self, col): """Single filter row with op and inputs.""" extra = getattr(col.filter, 'html_extra', {}) return self._render_jinja( - ''' + """ {{renderer.filtering_col_label(col)}} {{renderer.filtering_col_op_select(col)}} @@ -440,7 +426,7 @@ def filtering_table_row(self, col): - ''', + """, renderer=self, col=col, extra=extra, @@ -459,27 +445,27 @@ def filtering_col_op_select(self, col): current_selected = filter.op primary_op = filter.primary_op or filter.operators[0] - is_primary = lambda op: 'primary' if op == primary_op else None # noqa: E731 + is_primary = lambda op: 'primary' if op == primary_op else None - field_name = 'op({0})'.format(col.key) + field_name = f'op({col.key})' field_name = self.grid.prefix_qs_arg_key(field_name) return self.render_select( [(op.key, op.display, is_primary(op)) for op in filter.operators], current_selected, - name=field_name + name=field_name, ) def filtering_col_inputs1(self, col): """Render the first input, which can be freeform or select.""" filter = col.filter - field_name = 'v1({0})'.format(col.key) + field_name = f'v1({col.key})' field_name = self.grid.prefix_qs_arg_key(field_name) inputs = Markup() if 'input' in filter.input_types: - ident = '{0}_input1'.format(col.key) + ident = f'{col.key}_input1' inputs += self._render_jinja( '', attrs=dict( @@ -487,7 +473,7 @@ def filtering_col_inputs1(self, col): value=filter.value1_set_with, id=ident, type='text', - ) + ), ) if 'select' in filter.input_types: current_selected = tolist(filter.value1) or [] @@ -496,13 +482,13 @@ def filtering_col_inputs1(self, col): current_selection=current_selected, placeholder=None, multiple=filter.receives_list, - name=field_name + name=field_name, ) if filter.receives_list: inputs += self.filtering_multiselect( field_name, current_selected, - self.filtering_filter_options_multi(filter, field_name) + self.filtering_filter_options_multi(filter, field_name), ) return inputs @@ -514,7 +500,7 @@ def filtering_multiselect(self, field_name, current_selected, options): main render/transform here. """ return self._render_jinja( - ''' + """
- ''', + """, field_name=field_name, current_selected=current_selected, options=options, @@ -557,7 +543,7 @@ def filtering_filter_options_multi(self, filter, field_name): if inspect.isclass(validator): validator = validator() return self._render_jinja( - ''' + """ {% for value, label in filter.options_seq %}
  • {% endfor %} - ''', + """, filter=filter, field_name=field_name, selected=selected, @@ -581,29 +567,24 @@ def filtering_filter_options_multi(self, filter, field_name): def filtering_col_inputs2(self, col): """Render the second filter input, currently only a freeform.""" filter = col.filter - field_name = 'v2({0})'.format(col.key) + field_name = f'v2({col.key})' field_name = self.grid.prefix_qs_arg_key(field_name) if 'input2' not in filter.input_types: return Markup('') # field will get modified by JS - ident = '{0}_input2'.format(col.key) + ident = f'{col.key}_input2' return self._render_jinja( '', - attrs=dict( - name=field_name, - value=filter.value2_set_with, - id=ident, - type='text' - ) + attrs=dict(name=field_name, value=filter.value2_set_with, id=ident, type='text'), ) def filtering_add_filter_select(self): """Render the select box for adding a new filter. Used by the filter template.""" return self.render_select( [(col.key, col.label) for col in self.grid.filtered_cols.values()], - name='datagrid-add-filter' + name='datagrid-add-filter', ) def filtering_json_data(self): @@ -631,17 +612,21 @@ def confirm_export(self): confirmation_required = False else: confirmation_required = count > self.grid.unconfirmed_export_limit - return jsonmod.dumps({ - 'confirm_export': confirmation_required, - 'record_count': count - }) + return jsonmod.dumps({'confirm_export': confirmation_required, 'record_count': count}) def header_sorting(self): """Render the sort area. Used by the header template.""" return self.load_content('header_sorting.html') - def render_select(self, options, current_selection=None, placeholder=('', Markup(' ')), - name=None, id=None, **kwargs): + def render_select( + self, + options, + current_selection=None, + placeholder=('', Markup(' ')), + name=None, + id=None, + **kwargs, + ): """Generalized select box renderer. Args: @@ -673,7 +658,7 @@ def render_select(self, options, current_selection=None, placeholder=('', Markup kwargs['id'] = id return self._render_jinja( - ''' + """ {% for value, label, data in options %}