diff --git a/.github/workflows/prek.yml b/.github/workflows/prek.yml new file mode 100644 index 0000000..917b259 --- /dev/null +++ b/.github/workflows/prek.yml @@ -0,0 +1,27 @@ +name: "Prek Checks" +on: + push: + branches: [master] + pull_request: +permissions: {} +jobs: + prek: + name: "Run prek" + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: "Checkout code" + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: "Install uv" + uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 + with: + enable-cache: true + - name: "Set up Python" + run: uv python install + - name: "Install dependencies" + run: uv sync --locked --all-extras --dev + - name: "Run prek" + uses: j178/prek-action@4e14d07f9231acabce116ccfca13b13dd9755ece # v3.0.0 diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index a186c75..343643b 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -1,69 +1,48 @@ -name: Release theme to PyPI - -permissions: - contents: write - id-token: write - +name: "PyPI Release" on: - push: - tags: - - v2.* - + workflow_dispatch: +permissions: {} jobs: build: - name: build dist files + name: "Build distributions" + # Only publish from a release tag, never from a branch. + if: startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest - + permissions: + contents: read steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v5 - with: - python-version: 3.11 - - - name: install build - run: python -m pip install --upgrade build - - - name: build dist - run: python -m build - - - uses: actions/upload-artifact@v4 - with: - name: artifacts - path: dist/* - if-no-files-found: error - - publish: + - name: "Checkout code" + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: "Set up python" + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version-file: .python-version + - name: "Install Hatch" + run: pipx install hatch + - name: "Build package" + run: hatch build + - name: "Upload distributions" + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: dist + path: dist/ + if-no-files-found: error + pypi-publish: + name: "Upload release to PyPI" + needs: build + runs-on: ubuntu-latest environment: name: pypi-release url: https://pypi.org/p/livereload - - name: release to pypi - needs: build - runs-on: ubuntu-latest - - steps: - - uses: actions/download-artifact@v4 - with: - name: artifacts - path: dist - - - name: Push build artifacts to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - - release: - name: write release note - runs-on: ubuntu-latest - needs: publish - + permissions: + id-token: write steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - uses: actions/setup-node@v4 - with: - node-version: 18 - - run: npx changelogithub --no-group - continue-on-error: true - env: - GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + - name: "Download distributions" + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: dist + path: dist/ + - name: "Publish package distributions to PyPI" + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 0000000..2c4b9cb --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,31 @@ +name: "Run Tests (pytest)" +on: + push: + branches: [master] + pull_request: + workflow_dispatch: +permissions: {} +jobs: + tests: + name: "Run pytest" + runs-on: ubuntu-latest + permissions: + contents: read + strategy: + fail-fast: false + matrix: + python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] + steps: + - name: "Checkout code" + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: "Install uv and set up Python version" + uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 + with: + enable-cache: true + python-version: ${{ matrix.python-version }} + - name: "Install dependencies" + run: uv sync --locked --all-extras --dev + - name: "Run pytest" + run: uv run pytest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ff63178 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,28 @@ +name: "GitHub Release" +on: + push: + tags: ["v*"] +permissions: {} +jobs: + release: + name: "Write release notes" + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: "Checkout code" + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + fetch-depth: 0 + # NOTE: changing to gh specific actions can help drop node setup + - name: "Setup node" + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: 24 + package-manager-cache: false + - name: "Generate changelog" + run: npx changelogithub --no-group + continue-on-error: true + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..396af97 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,109 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +default_install_hook_types: ["pre-commit", "commit-msg", "pre-push"] + +repos: + - repo: "https://github.com/compilerla/conventional-pre-commit" + rev: "v4.4.0" + hooks: + - id: "conventional-pre-commit" + stages: ["commit-msg"] + + - repo: "https://github.com/pre-commit/pre-commit-hooks" + rev: "v6.0.0" + hooks: + - id: "trailing-whitespace" + - id: "end-of-file-fixer" + - id: "check-toml" + - id: "check-yaml" + - id: "mixed-line-ending" + - id: "check-merge-conflict" + - id: "detect-private-key" + - id: "check-shebang-scripts-are-executable" + - id: "check-executables-have-shebangs" + + - repo: "meta" + hooks: + - id: "check-useless-excludes" + + - repo: "https://github.com/gitleaks/gitleaks" + rev: "v8.30.1" + hooks: + - id: "gitleaks" + + - repo: "https://github.com/zizmorcore/zizmor-pre-commit" + rev: "v1.30.1" + hooks: + - id: "zizmor" + + # - repo: "https://github.com/astral-sh/ruff-pre-commit" + # rev: "v0.16.10" + # hooks: + # - id: "ruff-format" + # - id: "ruff-check" + + # - repo: "https://github.com/DetachHead/basedpyright-prek-mirror" + # rev: "1.40.1" + # hooks: + # - id: "basedpyright" + + - repo: "https://github.com/tox-dev/pyproject-fmt" + rev: "v2.30.0" + hooks: + - id: "pyproject-fmt" + files: "^pyproject\\.toml$" + + - repo: "https://github.com/tombi-toml/tombi-pre-commit" + rev: "v1.7.1" + hooks: + - id: "tombi-format" + exclude: "(^pyproject\\.toml$|^uv\\.lock$)" + - id: "tombi-lint" + + - repo: "https://github.com/google/yamlfmt" + rev: "v0.21.0" + hooks: + - id: "yamlfmt" + + - repo: "https://github.com/rhysd/actionlint" + rev: "v1.7.12" + hooks: + - id: "actionlint" + files: "^\\.github/workflows/.*\\.ya?ml$" + + - repo: "https://github.com/adrienverge/yamllint.git" + rev: "v1.38.0" + hooks: + - id: "yamllint" + exclude: "^\\.github/workflows/.*\\.ya?ml$" + + - repo: "https://github.com/rvben/rumdl-pre-commit" + rev: "v0.2.78" + hooks: + - id: "rumdl-fmt" + - id: "rumdl" + + - repo: "https://github.com/JoC0de/pre-commit-prettier" + rev: "v3.9.9" + hooks: + - id: "prettier" + types_or: ["html", "css", "javascript"] + + - repo: "https://github.com/codespell-project/codespell" + rev: "v2.4.3" + hooks: + - id: "codespell" + + - repo: "https://github.com/astral-sh/uv-pre-commit" + rev: "0.12.23" + hooks: + - id: "uv-lock" + + - repo: "local" + hooks: + - id: "pytest" + name: "pytest" + entry: "uv run pytest" + pass_filenames: false + language: "system" + stages: ["pre-push"] diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..4f70101 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +src/livereload/vendors/livereload.js diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..6324d40 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/.yamllint.yaml b/.yamllint.yaml new file mode 100644 index 0000000..f53eec5 --- /dev/null +++ b/.yamllint.yaml @@ -0,0 +1,3 @@ +extends: default +rules: + document-start: disable diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 2d661dd..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,3 +0,0 @@ -include livereload/vendors/livereload.js -include LICENSE -include README.rst diff --git a/Makefile b/Makefile index fa27900..59df01f 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ -.PHONY: clean-pyc clean-build docs test coverage +# .PHONY: clean-pyc clean-build docs test coverage lint typecheck check build install +.PHONY: clean-pyc clean-build docs test coverage check build install clean: clean-build clean-pyc @@ -15,14 +16,25 @@ clean-pyc: find . -name '*~' -exec rm -f {} + install: - python3 setup.py install + uv sync + +build: + uv build docs: $(MAKE) -C docs html test: - @nosetests -s + uv run pytest + +# lint: +# uv run ruff check . + +# typecheck: +# uv run basedpyright + +# check: lint typecheck test +check: test coverage: - @rm -f .coverage - @nosetests --with-coverage --cover-package=livereload --cover-html + uv run --with pytest-cov pytest --cov=livereload --cov-report=html diff --git a/docs/conf.py b/docs/conf.py index 7b7bcc8..a2430e8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,4 @@ -"""A sphinx documentation configuration file. -""" +"""A sphinx documentation configuration file.""" # -- Project information --------------------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information diff --git a/example/index.html b/example/index.html index 56c7031..ee08971 100644 --- a/example/index.html +++ b/example/index.html @@ -1,12 +1,12 @@ - + - - - Python LiveReload Example - - - -

Example

-

Container

- + + + Python LiveReload Example + + + +

Example

+

Container

+ diff --git a/example/server.py b/example/server.py index f0960b6..6991229 100644 --- a/example/server.py +++ b/example/server.py @@ -1,7 +1,5 @@ -#!/usr/bin/env python - from livereload import Server, shell server = Server() -server.watch('style.less', shell('lessc style.less', output='style.css')) +server.watch("style.less", shell("lessc style.less", output="style.css")) server.serve(open_url_delay=1) diff --git a/livereload/__init__.py b/livereload/__init__.py deleted file mode 100644 index aeb788c..0000000 --- a/livereload/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -""" - livereload - ~~~~~~~~~~ - - A python version of livereload. - - :copyright: (c) 2013 by Hsiaoming Yang - :license: BSD, see LICENSE for more details. -""" - -__version__ = '2.7.1' -__author__ = 'Hsiaoming Yang ' -__homepage__ = 'https://github.com/lepture/python-livereload' - -from .server import Server, shell - -__all__ = ('Server', 'shell') diff --git a/livereload/cli.py b/livereload/cli.py deleted file mode 100644 index a50f917..0000000 --- a/livereload/cli.py +++ /dev/null @@ -1,61 +0,0 @@ -import argparse - -import tornado.log - -from livereload.server import Server - - -parser = argparse.ArgumentParser(description='Start a `livereload` server') -parser.add_argument( - '--host', - help='Hostname to run `livereload` server on', - type=str, - default='127.0.0.1' -) -parser.add_argument( - '-p', '--port', - help='Port to run `livereload` server on', - type=int, - default=35729 -) -parser.add_argument( - 'directory', - help='Directory to serve files from', - type=str, - default='.', - nargs='?' -) -parser.add_argument( - '-t', '--target', - help='File or directory to watch for changes', - type=str, -) -parser.add_argument( - '-w', '--wait', - help='Time delay in seconds before reloading', - type=float, - default=0.0 -) -parser.add_argument( - '-o', '--open-url-delay', - help='If set, triggers browser opening seconds after starting', - type=float -) -parser.add_argument( - '-d', '--debug', - help='Enable Tornado pretty logging', - action='store_true' -) - - -def main(argv=None): - args = parser.parse_args() - - if args.debug: - tornado.log.enable_pretty_logging() - - # Create a new application - server = Server() - server.watcher.watch(args.target or args.directory, delay=args.wait) - server.serve(host=args.host, port=args.port, root=args.directory, - open_url_delay=args.open_url_delay) diff --git a/livereload/management/commands/livereload.py b/livereload/management/commands/livereload.py deleted file mode 100644 index 1b79fba..0000000 --- a/livereload/management/commands/livereload.py +++ /dev/null @@ -1,43 +0,0 @@ -import os -import re -from django.core.management.base import BaseCommand, CommandError -from django.core.management.commands.runserver import naiveip_re -from django.core.servers.basehttp import get_internal_wsgi_application -from livereload import Server - - -class Command(BaseCommand): - help = 'Runs the development server with livereload enabled.' - - def add_arguments(self, parser): - parser.add_argument('addrport', - nargs='?', - default='127.0.0.1:8000', - help='host and optional port the django server should listen on (default: 127.0.0.1:8000)') - parser.add_argument('-l', '--liveport', - type=int, - default=35729, - help='port the livereload server should listen on (default: 35729)') - - def handle(self, *args, **options): - m = re.match(naiveip_re, options['addrport']) - if m is None: - raise CommandError('"%s" is not a valid port number ' - 'or address:port pair.' % options['addrport']) - addr, _ipv4, _ipv6, _fqdn, port = m.groups() - if not port.isdigit(): - raise CommandError("%r is not a valid port number." % port) - - if addr: - if _ipv6: - raise CommandError('IPv6 addresses are currently not supported.') - - - application = get_internal_wsgi_application() - server = Server(application) - - for file in os.listdir('.'): - if file[0] != '.' and file[:2] != '__' and os.path.isdir(file): - server.watch(file) - - server.serve(host=addr, port=port, liveport=options['liveport']) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5365c7f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,95 @@ +[build-system] +build-backend = "hatchling.build" +requires = [ "hatchling>=1.26" ] + +[project] +name = "livereload" +description = "Python LiveReload is an awesome tool for web developers" +readme = "README.md" +keywords = [ + "autoreload", + "browser-refresh", + "development-server", + "django", + "file-watcher", + "live-reload", + "livereload", + "static-server", + "tornado", + "wsgi", +] +license = "BSD-3-Clause" +license-files = [ "LICEN[CS]E*" ] +authors = [ { name = "Hsiaoming Yang", email = "me@lepture.com" } ] +requires-python = ">=3.10" +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Environment :: Web Environment :: Mozilla", + "Intended Audience :: Developers", + "Natural Language :: English", + "Operating System :: MacOS :: MacOS X", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Compilers", + "Topic :: Software Development :: Debuggers", +] +dynamic = [ "version" ] +dependencies = [ + # 6.1 handles the default Windows proactor event loop on its own. + "tornado>=6.1", + # typing.override is only available from Python 3.12. + "typing-extensions>=4.4; python_version<'3.12'", +] +optional-dependencies.notify = [ + # pyinotify imports asyncore, which was removed in Python 3.12. + "pyinotify>=0.9.6; python_version<'3.12'", +] +urls.Changelog = "https://github.com/lepture/python-livereload/blob/master/CHANGES.rst" +urls.Documentation = "https://livereload.readthedocs.io" +urls.Issues = "https://github.com/lepture/python-livereload/issues" +urls.Repository = "https://github.com/lepture/python-livereload" +scripts.livereload = "livereload.cli:main" + +[dependency-groups] +dev = [ + "django-stubs>=6.0.9", + "prek>=0.5.5", + "pytest>=8.4.2", + "typing-extensions>=4.4", +] + +[tool.hatch] +version.path = "src/livereload/__init__.py" +build.targets.wheel.packages = [ "src/livereload" ] +build.targets.sdist.include = [ "/src", "/tests", "/docs", "/CHANGES.rst" ] + +[tool.ruff] +line-length = 80 +lint.extend-select = [ "ANN" ] +lint.unfixable = [ "F401" ] + +# Tests might fail in 3.10 and 3.11 +[tool.pytest] +ini_options.filterwarnings = [ + # pyinotify imports asyncore, deprecated on Python 3.10 and 3.11. + "ignore:The asyncore module is deprecated:DeprecationWarning", +] + +[tool.basedpyright] +# Check against the oldest supported Python +pythonVersion = "3.10" +reportUnusedCallResult = false +executionEnvironments = [ + { root = "tests", reportPrivateUsage = false }, +] diff --git a/rumdl.toml b/rumdl.toml new file mode 100644 index 0000000..1483955 --- /dev/null +++ b/rumdl.toml @@ -0,0 +1,24 @@ +[global] +disable = [ + "MD025", # Allow multiple h1 headings + "MD026", # Permit punctuation in heading + "MD033", # Permit inline HTML + "MD040", # Bare fences are intentional (shell snippets) + "MD041", # Allow first line to be anything (not just h1) +] +extend-enable = ["MD060"] # Table alignment +respect-gitignore = true + +[MD013] # Line length +line-length = 160 +code-blocks = false +tables = false +headings = false + +[MD060] # Table alignment +enabled = true +style = "aligned" + +[per-file-flavor] +# Sphinx docs use MyST directives such as ```{toctree}. +"docs/**/*.md" = "myst" diff --git a/server.py b/server.py index 0528a8a..2ba0bee 100644 --- a/server.py +++ b/server.py @@ -1,5 +1,5 @@ from livereload import Server, shell server = Server() -server.watch('docs/*.rst', shell('make html')) -server.serve(root='docs/_build/html') +server.watch("docs/*.rst", shell("make html")) +server.serve(root="docs/_build/html") diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 8183238..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[metadata] -license_files = LICENSE diff --git a/setup.py b/setup.py deleted file mode 100644 index 1cbe18c..0000000 --- a/setup.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python - -import re -from setuptools import setup - - -def fread(filepath): - with open(filepath) as f: - return f.read() - - -def version(): - content = fread('livereload/__init__.py') - pattern = r"__version__ = '([0-9\.dev]*)'" - m = re.findall(pattern, content) - return m[0] - - -setup( - name='livereload', - version=version(), - author='Hsiaoming Yang', - author_email='me@lepture.com', - url='https://github.com/lepture/python-livereload', - packages=['livereload', 'livereload.management.commands'], - description='Python LiveReload is an awesome tool for web developers', - long_description_content_type='text/x-rst', - long_description=fread('README.md'), - entry_points={ - 'console_scripts': [ - 'livereload = livereload.cli:main', - ] - }, - install_requires=[ - 'tornado', - ], - license='BSD', - include_package_data=True, - python_requires='>=3.7', - classifiers=[ - 'Development Status :: 4 - Beta', - 'Environment :: Console', - 'Environment :: Web Environment :: Mozilla', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Natural Language :: English', - 'Operating System :: MacOS :: MacOS X', - 'Operating System :: POSIX :: Linux', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: Implementation :: PyPy', - 'Topic :: Software Development :: Build Tools', - 'Topic :: Software Development :: Compilers', - 'Topic :: Software Development :: Debuggers', - ] -) diff --git a/src/livereload/__init__.py b/src/livereload/__init__.py new file mode 100644 index 0000000..ae97727 --- /dev/null +++ b/src/livereload/__init__.py @@ -0,0 +1,15 @@ +""" +livereload +~~~~~~~~~~ + +A python version of livereload. + +:copyright: (c) 2013 by Hsiaoming Yang +:license: BSD, see LICENSE for more details. +""" + +__version__ = "2.7.1" + +from .server import Server, shell + +__all__ = ("Server", "shell") diff --git a/livereload/__main__.py b/src/livereload/__main__.py similarity index 100% rename from livereload/__main__.py rename to src/livereload/__main__.py diff --git a/src/livereload/cli.py b/src/livereload/cli.py new file mode 100644 index 0000000..4ef7831 --- /dev/null +++ b/src/livereload/cli.py @@ -0,0 +1,94 @@ +import argparse +from typing import cast + +import tornado.log + +from livereload import __version__ as VERSION +from livereload.server import Server + +parser = argparse.ArgumentParser( + prog="livereload", + description="Start a `livereload` server", + allow_abbrev=False, +) + +parser.add_argument( + "-V", + "--version", + action="version", + version=f"%(prog)s {VERSION}", +) +parser.add_argument( + "-d", + "--debug", + help="enable Tornado pretty logging", + action="store_true", +) +parser.add_argument( + "--host", + help="hostname to run `livereload` server on", + type=str, + default="127.0.0.1", +) +parser.add_argument( + "-p", + "--port", + help="port to run `livereload` server on", + type=int, + default=35729, +) +parser.add_argument( + "directory", + help="directory to serve files from", + type=str, + default=".", + nargs="?", +) +parser.add_argument( + "-t", + "--target", + help="file or directory to watch for changes", + type=str, +) +parser.add_argument( + "-w", + "--wait", + help="time delay in seconds before reloading", + type=float, + default=0.0, + metavar="DELAY", +) +parser.add_argument( + "-o", + "--open-url-delay", + help="if set, triggers browser opening seconds after starting", + type=float, + metavar="DELAY", +) + + +def main(argv: list[str] | None = None) -> None: + args = parser.parse_args(argv) + host = cast(str, args.host) + port = cast(int, args.port) + directory = cast(str, args.directory) + target = cast(str | None, args.target) + wait = cast(float, args.wait) + open_url_delay = cast(float | None, args.open_url_delay) + + if cast(bool, args.debug): + tornado.log.enable_pretty_logging() + + # Create a new application + server = Server() + server.watcher.watch(target or directory, delay=wait) # pyright: ignore[reportArgumentType] + server.serve( + host=host, + port=port, + root=directory, + open_url_delay=open_url_delay, + ) + + +if __name__ == "__main__": + main() diff --git a/livereload/handlers.py b/src/livereload/handlers.py similarity index 75% rename from livereload/handlers.py rename to src/livereload/handlers.py index b8ec488..f3555ad 100644 --- a/livereload/handlers.py +++ b/src/livereload/handlers.py @@ -1,26 +1,26 @@ """ - livereload.handlers - ~~~~~~~~~~~~~~~~~~~ +livereload.handlers +~~~~~~~~~~~~~~~~~~~ - HTTP and WebSocket handlers for livereload. +HTTP and WebSocket handlers for livereload. - :copyright: (c) 2013 by Hsiaoming Yang - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013 by Hsiaoming Yang +:license: BSD, see LICENSE for more details. """ + import datetime import hashlib +import logging import os import stat import time -import logging -from tornado import web -from tornado import ioloop -from tornado import escape + +from tornado import escape, ioloop, web from tornado.log import gen_log -from tornado.websocket import WebSocketHandler from tornado.util import ObjectDict +from tornado.websocket import WebSocketHandler -logger = logging.getLogger('livereload') +logger = logging.getLogger("livereload") class LiveReloadHandler(WebSocketHandler): @@ -47,7 +47,7 @@ def send_message(self, message): try: self.write_message(message) except: - logger.error('Error sending message', exc_info=True) + logger.error("Error sending message", exc_info=True) @classmethod def start_tasks(cls): @@ -55,31 +55,31 @@ def start_tasks(cls): return if not cls.watcher._tasks: - logger.info('Watch current working directory') + logger.info("Watch current working directory") cls.watcher.watch(os.getcwd()) cls._last_reload_time = time.time() - logger.info('Start watching changes') + logger.info("Start watching changes") if not cls.watcher.start(cls.poll_tasks): - logger.info('Start detecting changes') + logger.info("Start detecting changes") ioloop.PeriodicCallback(cls.poll_tasks, 800).start() @classmethod def poll_tasks(cls): filepath, delay = cls.watcher.examine() - if not filepath or delay == 'forever' or not cls.waiters: + if not filepath or delay == "forever" or not cls.waiters: return reload_time = LiveReloadHandler.DEFAULT_RELOAD_TIME if delay: reload_time = max(3 - delay, 1) - if filepath == '__livereload__': + if filepath == "__livereload__": reload_time = 0 if time.time() - cls._last_reload_time < reload_time: # if you changed lot of files in one time # it will refresh too many times - logger.info('Ignore: %s', filepath) + logger.info("Ignore: %s", filepath) return if delay: loop = ioloop.IOLoop.current() @@ -90,19 +90,19 @@ def poll_tasks(cls): @classmethod def reload_waiters(cls, path=None): logger.info( - 'Reload %s waiters: %s', + "Reload %s waiters: %s", len(cls.waiters), cls.watcher.filepath, ) if path is None: - path = cls.watcher.filepath or '*' + path = cls.watcher.filepath or "*" msg = { - 'command': 'reload', - 'path': path, - 'liveCSS': cls.live_css, - 'liveImg': True, + "command": "reload", + "path": path, + "liveCSS": cls.live_css, + "liveImg": True, } cls._last_reload_time = time.time() @@ -110,7 +110,7 @@ def reload_waiters(cls, path=None): try: waiter.write_message(msg) except: - logger.error('Error sending message', exc_info=True) + logger.error("Error sending message", exc_info=True) cls.waiters.remove(waiter) def on_message(self, message): @@ -121,18 +121,18 @@ def on_message(self, message): 3. client send 'info' """ message = ObjectDict(escape.json_decode(message)) - if message.command == 'hello': + if message.command == "hello": handshake = { - 'command': 'hello', - 'protocols': [ - 'http://livereload.com/protocols/official-7', + "command": "hello", + "protocols": [ + "http://livereload.com/protocols/official-7", ], - 'serverName': 'livereload-tornado', + "serverName": "livereload-tornado", } self.send_message(handshake) - if message.command == 'info' and 'url' in message: - logger.info('Browser Connected: %s' % message.url) + if message.command == "info" and "url" in message: + logger.info("Browser Connected: %s" % message.url) LiveReloadHandler.waiters.add(self) @@ -148,7 +148,8 @@ def get_content_modified_time(cls, abspath): """ stat_result = os.stat(abspath) modified = datetime.datetime.utcfromtimestamp( - stat_result[stat.ST_MTIME]) + stat_result[stat.ST_MTIME] + ) return modified @classmethod @@ -163,7 +164,9 @@ def get_content_version(cls, abspath): data = cls.get_content(abspath) hasher = hashlib.md5() - mtime_data = format(cls.get_content_modified_time(abspath), "%Y-%m-%d %H:%M:%S") + mtime_data = format( + cls.get_content_modified_time(abspath), "%Y-%m-%d %H:%M:%S" + ) hasher.update(mtime_data.encode()) @@ -211,20 +214,19 @@ def _load_version(abs_path): class LiveReloadJSHandler(web.RequestHandler): - def get(self): - self.set_header('Content-Type', 'application/javascript') + self.set_header("Content-Type", "application/javascript") root = os.path.abspath(os.path.dirname(__file__)) - js_file = os.path.join(root, 'vendors/livereload.js') - with open(js_file, 'rb') as f: + js_file = os.path.join(root, "vendors/livereload.js") + with open(js_file, "rb") as f: self.write(f.read()) class ForceReloadHandler(web.RequestHandler): def get(self): - path = self.get_argument('path', default=None) or '*' + path = self.get_argument("path", default=None) or "*" LiveReloadHandler.reload_waiters(path) - self.write('ok') + self.write("ok") class StaticFileHandler(MtimeStaticFileHandler): diff --git a/livereload/management/__init__.py b/src/livereload/management/__init__.py similarity index 100% rename from livereload/management/__init__.py rename to src/livereload/management/__init__.py diff --git a/livereload/management/commands/__init__.py b/src/livereload/management/commands/__init__.py similarity index 100% rename from livereload/management/commands/__init__.py rename to src/livereload/management/commands/__init__.py diff --git a/src/livereload/management/commands/livereload.py b/src/livereload/management/commands/livereload.py new file mode 100644 index 0000000..7c91a5d --- /dev/null +++ b/src/livereload/management/commands/livereload.py @@ -0,0 +1,51 @@ +import os +import re +from django.core.management.base import BaseCommand, CommandError +from django.core.management.commands.runserver import naiveip_re +from django.core.servers.basehttp import get_internal_wsgi_application +from livereload import Server + + +class Command(BaseCommand): + help = "Runs the development server with livereload enabled." + + def add_arguments(self, parser): + parser.add_argument( + "addrport", + nargs="?", + default="127.0.0.1:8000", + help="host and optional port the django server should listen on (default: 127.0.0.1:8000)", + ) + parser.add_argument( + "-l", + "--liveport", + type=int, + default=35729, + help="port the livereload server should listen on (default: 35729)", + ) + + def handle(self, *args, **options): + m = re.match(naiveip_re, options["addrport"]) + if m is None: + raise CommandError( + '"%s" is not a valid port number ' + "or address:port pair." % options["addrport"] + ) + addr, _ipv4, _ipv6, _fqdn, port = m.groups() + if not port.isdigit(): + raise CommandError("%r is not a valid port number." % port) + + if addr: + if _ipv6: + raise CommandError( + "IPv6 addresses are currently not supported." + ) + + application = get_internal_wsgi_application() + server = Server(application) + + for file in os.listdir("."): + if file[0] != "." and file[:2] != "__" and os.path.isdir(file): + server.watch(file) + + server.serve(host=addr, port=port, liveport=options["liveport"]) diff --git a/livereload/server.py b/src/livereload/server.py similarity index 77% rename from livereload/server.py rename to src/livereload/server.py index 2ff2058..0f4251f 100644 --- a/livereload/server.py +++ b/src/livereload/server.py @@ -1,60 +1,66 @@ """ - livereload.server - ~~~~~~~~~~~~~~~~~ +livereload.server +~~~~~~~~~~~~~~~~~ - WSGI app server for livereload. +WSGI app server for livereload. - :copyright: (c) 2013 - 2015 by Hsiaoming Yang - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013 - 2015 by Hsiaoming Yang +:license: BSD, see LICENSE for more details. """ +import errno +import logging import os -import time +import re import shlex -import logging +import sys import threading +import time import webbrowser -import re -from subprocess import Popen, PIPE +from subprocess import PIPE, Popen -from tornado.wsgi import WSGIContainer -from tornado.ioloop import IOLoop +from tornado import escape, httputil, web from tornado.autoreload import add_reload_hook -from tornado import web -from tornado import escape -from tornado import httputil +from tornado.ioloop import IOLoop from tornado.log import LogFormatter -from .handlers import LiveReloadHandler, LiveReloadJSHandler -from .handlers import ForceReloadHandler, StaticFileHandler -from .watcher import get_watcher_class - -import sys +from tornado.wsgi import WSGIContainer -import errno +from .handlers import ( + ForceReloadHandler, + LiveReloadHandler, + LiveReloadJSHandler, + StaticFileHandler, +) +from .watcher import get_watcher_class -if sys.version_info >= (3, 8) and sys.platform == 'win32': +if sys.version_info >= (3, 8) and sys.platform == "win32": import asyncio + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) -logger = logging.getLogger('livereload') +logger = logging.getLogger("livereload") -HEAD_END_RE = re.compile(br'', re.IGNORECASE) +HEAD_END_RE = re.compile(rb"", re.IGNORECASE) def inject_script_at_head(content, script): """Inject script before closing tag, case-insensitive.""" - return HEAD_END_RE.sub(lambda match: script + match.group(0), content, count=1) + return HEAD_END_RE.sub( + lambda match: script + match.group(0), content, count=1 + ) def set_header(fn, name, value): """Helper Function to Add HTTP headers to the server""" + def set_default_headers(self, *args, **kwargs): fn(self, *args, **kwargs) self.set_header(name, value) + return set_default_headers -def shell(cmd, output=None, mode='w', cwd=None, shell=False): +def shell(cmd, output=None, mode="w", cwd=None, shell=False): """Execute a shell command. You can add a shell command:: @@ -83,8 +89,9 @@ def shell(cmd, output=None, mode='w', cwd=None, shell=False): def run_shell(): try: - p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, cwd=cwd, - shell=shell) + p = Popen( + cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, cwd=cwd, shell=shell + ) except OSError as e: logger.error(e) if e.errno == errno.ENOENT: # file (command) not found @@ -111,14 +118,14 @@ def transform_first_chunk(self, status_code, headers, chunk, finishing): is_html = "html" in headers.get("Content-Type", "") if is_html and HEAD_END_RE.search(chunk): chunk = inject_script_at_head(chunk, self.script) - if 'Content-Length' in headers: - length = int(headers['Content-Length']) + len(self.script) - headers['Content-Length'] = str(length) + if "Content-Length" in headers: + length = int(headers["Content-Length"]) + len(self.script) + headers["Content-Length"] = str(length) return status_code, headers, chunk class LiveScriptContainer(WSGIContainer): - def __init__(self, wsgi_app, script=''): + def __init__(self, wsgi_app, script=""): self.wsgi_app = wsgi_app self.script = script @@ -132,7 +139,8 @@ def start_response(status, response_headers, exc_info=None): return response.append app_response = self.wsgi_app( - WSGIContainer(self.wsgi_app).environ(request), start_response) + WSGIContainer(self.wsgi_app).environ(request), start_response + ) try: response.extend(app_response) body = b"".join(response) @@ -142,7 +150,7 @@ def start_response(status, response_headers, exc_info=None): if not data: raise Exception("WSGI app did not call start_response") - status_code, reason = data["status"].split(' ', 1) + status_code, reason = data["status"].split(" ", 1) status_code = int(status_code) headers = data["headers"] header_set = {k.lower() for (k, v) in headers} @@ -153,22 +161,19 @@ def start_response(status, response_headers, exc_info=None): if status_code != 304: if "content-type" not in header_set: - headers.append(( - "Content-Type", - "application/octet-stream; charset=UTF-8" - )) + headers.append( + ("Content-Type", "application/octet-stream; charset=UTF-8") + ) if "content-length" not in header_set: headers.append(("Content-Length", str(len(body)))) if "server" not in header_set: headers.append(("Server", "LiveServer")) - start_line = httputil.ResponseStartLine( - "HTTP/1.1", status_code, reason - ) + start_line = httputil.ResponseStartLine("HTTP/1.1", status_code, reason) header_obj = httputil.HTTPHeaders() for key, value in headers: - if key.lower() == 'content-length': + if key.lower() == "content-length": value = str(len(body)) header_obj.add(key, value) request.connection.write_headers(start_line, header_obj, chunk=body) @@ -190,6 +195,7 @@ class Server: pyinotify and use INotifyWatcher() to avoid wasted CPU usage. """ + def __init__(self, app=None, watcher=None): self.root = None @@ -201,10 +207,10 @@ def __init__(self, app=None, watcher=None): self.SFH = StaticFileHandler def setHeader(self, name, value): - """Add or override HTTP headers at the at the beginning of the + """Add or override HTTP headers at the at the beginning of the request. - Once you have initialized a server, you can add one or more + Once you have initialized a server, you can add one or more headers before starting the server:: server.setHeader('Access-Control-Allow-Origin', '*') @@ -215,7 +221,8 @@ def setHeader(self, name, value): :param value: The value of the header field to be defined. """ StaticFileHandler.set_default_headers = set_header( - StaticFileHandler.set_default_headers, name, value) + StaticFileHandler.set_default_headers, name, value + ) self.SFH = StaticFileHandler def watch(self, filepath, func=None, delay=None, ignore=None): @@ -248,17 +255,16 @@ def alert(): self.watcher.watch(filepath, func, delay, ignore=ignore) - def application(self, port, host, liveport=None, debug=None, - live_css=True): + def application(self, port, host, liveport=None, debug=None, live_css=True): LiveReloadHandler.watcher = self.watcher LiveReloadHandler.live_css = live_css if debug is None and self.app: debug = True live_handlers = [ - (r'/livereload', LiveReloadHandler), - (r'/forcereload', ForceReloadHandler), - (r'/livereload.js', LiveReloadJSHandler) + (r"/livereload", LiveReloadHandler), + (r"/forcereload", ForceReloadHandler), + (r"/livereload.js", LiveReloadJSHandler), ] # The livereload.js snippet. @@ -267,16 +273,19 @@ def application(self, port, host, liveport=None, debug=None, live_script = ( '' + "document.head.appendChild(s);" + "})();" ) if liveport: live_script = escape.utf8(live_script % liveport) else: - live_script = escape.utf8(live_script % "(window.location.port || (window.location.protocol == 'https:' ? 443: 80))") + live_script = escape.utf8( + live_script + % "(window.location.port || (window.location.protocol == 'https:' ? 443: 80))" + ) web_handlers = self.get_web_handlers(live_script) @@ -286,16 +295,14 @@ class ConfiguredTransform(LiveScriptInjector): if not liveport: handlers = live_handlers + web_handlers app = web.Application( - handlers=handlers, - debug=debug, - transforms=[ConfiguredTransform] + handlers=handlers, debug=debug, transforms=[ConfiguredTransform] ) app.listen(port, address=host) else: app = web.Application( handlers=web_handlers, debug=debug, - transforms=[ConfiguredTransform] + transforms=[ConfiguredTransform], ) app.listen(port, address=host) live = web.Application(handlers=live_handlers, debug=False) @@ -304,17 +311,31 @@ class ConfiguredTransform(LiveScriptInjector): def get_web_handlers(self, script): if self.app: fallback = LiveScriptContainer(self.app, script) - return [(r'.*', web.FallbackHandler, {'fallback': fallback})] + return [(r".*", web.FallbackHandler, {"fallback": fallback})] return [ - (r'/(.*)', self.SFH, { - 'path': self.root or '.', - 'default_filename': self.default_filename, - }), + ( + r"/(.*)", + self.SFH, + { + "path": self.root or ".", + "default_filename": self.default_filename, + }, + ), ] - def serve(self, port=5500, liveport=None, host=None, root=None, debug=None, - open_url=False, restart_delay=2, open_url_delay=None, - live_css=True, default_filename='index.html'): + def serve( + self, + port=5500, + liveport=None, + host=None, + root=None, + debug=None, + open_url=False, + restart_delay=2, + open_url_delay=None, + live_css=True, + default_filename="index.html", + ): """Start serve the server with the given port. :param port: serve on this port, default is 5500 @@ -329,30 +350,32 @@ def serve(self, port=5500, liveport=None, host=None, root=None, debug=None, Defaults to True :param default_filename: launch this file from the selected root on startup """ - host = host or '127.0.0.1' + host = host or "127.0.0.1" if root is not None: self.root = root self._setup_logging() - logger.info(f'Serving on http://{host}:{port}') + logger.info(f"Serving on http://{host}:{port}") self.default_filename = default_filename self.application( - port, host, liveport=liveport, debug=debug, live_css=live_css) + port, host, liveport=liveport, debug=debug, live_css=live_css + ) # Async open web browser after 5 sec timeout if open_url: - logger.error('Use `open_url_delay` instead of `open_url`') + logger.error("Use `open_url_delay` instead of `open_url`") if open_url_delay is not None: def opener(): time.sleep(open_url_delay) - webbrowser.open(f'http://{host}:{port}') + webbrowser.open(f"http://{host}:{port}") + threading.Thread(target=opener).start() try: - self.watcher._changes.append(('__livereload__', restart_delay)) + self.watcher._changes.append(("__livereload__", restart_delay)) LiveReloadHandler.start_tasks() # When autoreload is triggered, initiate a shutdown of the IOLoop add_reload_hook(lambda: IOLoop.instance().stop()) @@ -361,7 +384,7 @@ def opener(): # Once the IOLoop is stopped, the IOLoop can be closed to free resources IOLoop.current().close(all_fds=True) except KeyboardInterrupt: - logger.info('Shutting down...') + logger.info("Shutting down...") def _setup_logging(self): logger.setLevel(logging.INFO) @@ -371,4 +394,4 @@ def _setup_logging(self): logger.addHandler(channel) # need a tornado logging handler to prevent IOLoop._setup_logging - logging.getLogger('tornado').addHandler(channel) + logging.getLogger("tornado").addHandler(channel) diff --git a/livereload/vendors/livereload.js b/src/livereload/vendors/livereload.js similarity index 100% rename from livereload/vendors/livereload.js rename to src/livereload/vendors/livereload.js diff --git a/livereload/watcher.py b/src/livereload/watcher.py similarity index 85% rename from livereload/watcher.py rename to src/livereload/watcher.py index 09c3a1b..72805ea 100644 --- a/livereload/watcher.py +++ b/src/livereload/watcher.py @@ -1,11 +1,11 @@ """ - livereload.watcher - ~~~~~~~~~~~~~~~~~~ +livereload.watcher +~~~~~~~~~~~~~~~~~~ - A file watch management for LiveReload Server. +A file watch management for LiveReload Server. - :copyright: (c) 2013 - 2015 by Hsiaoming Yang - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013 - 2015 by Hsiaoming Yang +:license: BSD, see LICENSE for more details. """ import glob @@ -19,11 +19,12 @@ except ImportError: pyinotify = None -logger = logging.getLogger('livereload') +logger = logging.getLogger("livereload") class Watcher: """A file watcher registry.""" + def __init__(self): self._tasks = {} @@ -40,7 +41,7 @@ def __init__(self): self._start = time.time() # list of ignored dirs - self.ignored_dirs = ['.git', '.hg', '.svn', '.cvs'] + self.ignored_dirs = [".git", ".hg", ".svn", ".cvs"] def ignore_dirs(self, *args): self.ignored_dirs.extend(args) @@ -52,7 +53,7 @@ def remove_dirs_from_ignore(self, *args): def ignore(self, filename): """Ignore a given filename or not.""" _, ext = os.path.splitext(filename) - return ext in ['.pyc', '.pyo', '.o', '.swp'] + return ext in [".pyc", ".pyo", ".o", ".swp"] def watch(self, path, func=None, delay=0, ignore=None): """Add a task to watcher. @@ -66,10 +67,10 @@ def watch(self, path, func=None, delay=0, ignore=None): filepath. """ self._tasks[path] = { - 'func': func, - 'delay': delay, - 'ignore': ignore, - 'mtimes': {}, + "func": func, + "delay": delay, + "ignore": ignore, + "mtimes": {}, } def start(self, callback): @@ -90,20 +91,21 @@ def examine(self): delays = set() for path in self._tasks: item = self._tasks[path] - self._task_mtimes = item['mtimes'] - changed = self.is_changed(path, item['ignore']) + self._task_mtimes = item["mtimes"] + changed = self.is_changed(path, item["ignore"]) if changed: - func = item['func'] - delay = item['delay'] + func = item["func"] + delay = item["delay"] if delay and isinstance(delay, float): delays.add(delay) if func: - name = getattr(func, 'name', None) + name = getattr(func, "name", None) if not name: - name = getattr(func, '__name__', 'anonymous') - logger.info( - f"Running task: {name} (delay: {delay})") - if len(signature(func).parameters) > 0 and isinstance(changed, list): + name = getattr(func, "__name__", "anonymous") + logger.info(f"Running task: {name} (delay: {delay})") + if len(signature(func).parameters) > 0 and isinstance( + changed, list + ): func(changed) else: func() @@ -222,15 +224,17 @@ def start(self, callback): self.callback = callback from tornado import ioloop + self.notifier = pyinotify.TornadoAsyncNotifier( - self.wm, ioloop.IOLoop.instance(), - default_proc_fun=self.inotify_event + self.wm, + ioloop.IOLoop.instance(), + default_proc_fun=self.inotify_event, ) callback() return True def get_watcher_class(): - if pyinotify is None or not hasattr(pyinotify, 'TornadoAsyncNotifier'): + if pyinotify is None or not hasattr(pyinotify, "TornadoAsyncNotifier"): return Watcher return INotifyWatcher diff --git a/tests/test_server.py b/tests/test_server.py index 152d3fb..2b9d6fc 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -7,11 +7,7 @@ class TestInjectScriptAtHead(unittest.TestCase): def test_injects_before_mixed_case_head_end(self): content = b"x" script = b"" - expected = ( - b"x" - + script - + b"" - ) + expected = b"x" + script + b"" assert inject_script_at_head(content, script) == expected def test_injects_only_first_head_end(self): diff --git a/tests/test_watcher.py b/tests/test_watcher.py index 07ee21c..d8cd847 100644 --- a/tests/test_watcher.py +++ b/tests/test_watcher.py @@ -1,18 +1,16 @@ -#!/usr/bin/python - import os -import time import shutil +import time import unittest + from livereload.watcher import get_watcher_class Watcher = get_watcher_class() -tmpdir = os.path.join(os.path.dirname(__file__), 'tmp') +tmpdir = os.path.join(os.path.dirname(__file__), "tmp") class TestWatcher(unittest.TestCase): - def setUp(self): if os.path.isdir(tmpdir): shutil.rmtree(tmpdir) @@ -22,10 +20,10 @@ def tearDown(self): shutil.rmtree(tmpdir) def test_watch_dir(self): - os.mkdir(os.path.join(tmpdir, '.git')) - os.mkdir(os.path.join(tmpdir, '.hg')) - os.mkdir(os.path.join(tmpdir, '.svn')) - os.mkdir(os.path.join(tmpdir, '.cvs')) + os.mkdir(os.path.join(tmpdir, ".git")) + os.mkdir(os.path.join(tmpdir, ".hg")) + os.mkdir(os.path.join(tmpdir, ".svn")) + os.mkdir(os.path.join(tmpdir, ".cvs")) watcher = Watcher() watcher.watch(tmpdir) @@ -35,10 +33,10 @@ def test_watch_dir(self): # TODO: This doesn't seem necessary; test passes without it time.sleep(1) - filepath = os.path.join(tmpdir, 'foo') + filepath = os.path.join(tmpdir, "foo") - with open(filepath, 'w') as f: - f.write('') + with open(filepath, "w") as f: + f.write("") assert watcher.is_changed(tmpdir) assert watcher.is_changed(tmpdir) is False @@ -55,9 +53,9 @@ def test_watch_file(self): # TODO: This doesn't seem necessary; test passes without it time.sleep(1) - filepath = os.path.join(tmpdir, 'foo') - with open(filepath, 'w') as f: - f.write('') + filepath = os.path.join(tmpdir, "foo") + with open(filepath, "w") as f: + f.write("") def add_count(): watcher.count += 1 @@ -70,8 +68,8 @@ def add_count(): # TODO: This doesn't seem necessary; test passes without it time.sleep(1) - with open(filepath, 'w') as f: - f.write('') + with open(filepath, "w") as f: + f.write("") abs_filepath = os.path.abspath(filepath) assert watcher.examine() == (abs_filepath, None) @@ -85,18 +83,18 @@ def add_count(): def test_watch_glob(self): watcher = Watcher() - watcher.watch(tmpdir + '/*') + watcher.watch(tmpdir + "/*") assert watcher.examine() == (None, None) - with open(os.path.join(tmpdir, 'foo.pyc'), 'w') as f: - f.write('') + with open(os.path.join(tmpdir, "foo.pyc"), "w") as f: + f.write("") assert watcher.examine() == (None, None) - filepath = os.path.join(tmpdir, 'foo') + filepath = os.path.join(tmpdir, "foo") - with open(filepath, 'w') as f: - f.write('') + with open(filepath, "w") as f: + f.write("") abs_filepath = os.path.abspath(filepath) assert watcher.examine() == (abs_filepath, None) @@ -108,17 +106,17 @@ def test_watch_glob(self): def test_watch_ignore(self): watcher = Watcher() - watcher.watch(tmpdir + '/*', ignore=lambda o: o.endswith('.ignore')) + watcher.watch(tmpdir + "/*", ignore=lambda o: o.endswith(".ignore")) assert watcher.examine() == (None, None) - with open(os.path.join(tmpdir, 'foo.ignore'), 'w') as f: - f.write('') + with open(os.path.join(tmpdir, "foo.ignore"), "w") as f: + f.write("") assert watcher.examine() == (None, None) def test_watch_multiple_dirs(self): - first_dir = os.path.join(tmpdir, 'first') - second_dir = os.path.join(tmpdir, 'second') + first_dir = os.path.join(tmpdir, "first") + second_dir = os.path.join(tmpdir, "second") watcher = Watcher() @@ -126,9 +124,9 @@ def test_watch_multiple_dirs(self): watcher.watch(first_dir) assert watcher.examine() == (None, None) - first_path = os.path.join(first_dir, 'foo') - with open(first_path, 'w') as f: - f.write('') + first_path = os.path.join(first_dir, "foo") + with open(first_path, "w") as f: + f.write("") assert watcher.examine() == (first_path, None) assert watcher.examine() == (None, None) @@ -136,14 +134,14 @@ def test_watch_multiple_dirs(self): watcher.watch(second_dir) assert watcher.examine() == (None, None) - second_path = os.path.join(second_dir, 'bar') - with open(second_path, 'w') as f: - f.write('') + second_path = os.path.join(second_dir, "bar") + with open(second_path, "w") as f: + f.write("") assert watcher.examine() == (second_path, None) assert watcher.examine() == (None, None) - with open(first_path, 'a') as f: - f.write('foo') + with open(first_path, "a") as f: + f.write("foo") assert watcher.examine() == (first_path, None) assert watcher.examine() == (None, None) diff --git a/tombi.toml b/tombi.toml new file mode 100644 index 0000000..dc37d0b --- /dev/null +++ b/tombi.toml @@ -0,0 +1,2 @@ +[format.rules] +line-width = 80 diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..e1416e4 --- /dev/null +++ b/uv.lock @@ -0,0 +1,389 @@ +version = 1 +revision = 5 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version < '3.11'", +] + +[[package]] +name = "asgiref" +version = "3.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/26/3b59f2bdae5f640389becb1f673cded775287f5fc4f816309d9ca9a3f93d/asgiref-3.12.1.tar.gz", hash = "sha256:59dcb51c272ad209d59bed5708a64a333083e86017d7fcdd67498eeab7784340", size = 42378, upload-time = "2026-07-14T09:56:18.087Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/1b/54f4ad77cd8a584fa70746c47df988e002cf1ee1eba43364d46f87803647/asgiref-3.12.1-py3-none-any.whl", hash = "sha256:fe386d1c2bff7259ea95929266d12a8cf9a8b5a1c2598402967d8792e7a7c094", size = 25478, upload-time = "2026-07-14T09:56:16.926Z" }, +] + +[[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 = "django" +version = "5.2.18" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*'", + "python_full_version < '3.11'", +] +dependencies = [ + { name = "asgiref" }, + { name = "sqlparse" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/fe/79e692b430c7721bf4b812c328bfe687e73e3f28f56611a6a18a97e887cc/django-5.2.18.tar.gz", hash = "sha256:461c5dd06d2ea16bd5ca37d3f46e4def1d6b0fe7588c6f4e2119517bb0af8b2d", size = 10910238, upload-time = "2026-10-06T13:01:09.371Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/01/6568ec52b26548ca1b83d33249f7274db3bf74698f42c0da42ec9db1a24a/django-5.2.18-py3-none-any.whl", hash = "sha256:92ed81d500be6408ecd704d7bd1366c534f30427bffcc63c5fefb129561aec7c", size = 8316071, upload-time = "2026-10-06T13:01:04.413Z" }, +] + +[[package]] +name = "django" +version = "6.1.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", +] +dependencies = [ + { name = "asgiref" }, + { name = "sqlparse" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/24/20d187a22ac821288b53c53b8f696019dcb0c7b4f973f052ade19392c74d/django-6.1.2.tar.gz", hash = "sha256:a1e92451ccb8b514e91bbb3b6d186d20b4030558f116b5d9de6535455ff210b7", size = 11251425, upload-time = "2026-10-06T12:53:29.622Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/83/90ff2dbfac7b519ed77620bdbb827cd2d1a83faf00d076bf95056428bce7/django-6.1.2-py3-none-any.whl", hash = "sha256:141efee6ec64d1db6db90683bf734c550102450f444fb099063b0be1bd27d991", size = 8420684, upload-time = "2026-10-06T12:53:17.381Z" }, +] + +[[package]] +name = "django-stubs" +version = "6.0.9" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "django", version = "5.2.18", source = { registry = "https://pypi.org/simple" } }, + { name = "django-stubs-ext", version = "6.0.9", source = { registry = "https://pypi.org/simple" } }, + { name = "tomli" }, + { name = "types-pyyaml" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/8f/f761e2d1cdbfce2a10fba4b2d170657cbf54592d5b280823f56f896ffcc0/django_stubs-6.0.9.tar.gz", hash = "sha256:ef9928e900a9d226162b579b50c035d08755f6279fd47de9f427de4123c23623", size = 288505, upload-time = "2026-08-07T12:58:01.987Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/7a/c3ee425478eafe63a568bf76015524a25e9b889f29694019551209bd4daf/django_stubs-6.0.9-py3-none-any.whl", hash = "sha256:dbee54721197b453708b39bb6b5988b6543eddf3e5073b8772c13bf4652af17d", size = 549598, upload-time = "2026-08-07T12:57:59.9Z" }, +] + +[[package]] +name = "django-stubs" +version = "6.1.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "django", version = "5.2.18", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "django", version = "6.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "django-stubs-ext", version = "6.1.2", source = { registry = "https://pypi.org/simple" } }, + { name = "types-pyyaml" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/d0/00a9af43bf65ee54706e29b1c41b3a00d7a97157fbf97ed3038dcc73d584/django_stubs-6.1.2.tar.gz", hash = "sha256:d62b15fa5cbae79b1922d015e1454d299959d230221dadb23f65c749480b97ec", size = 295860, upload-time = "2026-10-05T06:47:17.595Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/36/206be00ba6aaf21c72cf61fd38cee492c7dcf51eac72fd61a9f7ddd0f916/django_stubs-6.1.2-py3-none-any.whl", hash = "sha256:ffb6e74f54b8c1f9b2f3bc4852c5abbaa2324b2a7cb1c51854c5f95abe67165c", size = 559846, upload-time = "2026-10-05T06:47:15.889Z" }, +] + +[[package]] +name = "django-stubs-ext" +version = "6.0.9" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "django", version = "5.2.18", source = { registry = "https://pypi.org/simple" } }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/1f/7a4c5f1c1b1b6573623c91c247dfe1f226ed14fa0690e218842bfb5ff88a/django_stubs_ext-6.0.9.tar.gz", hash = "sha256:6777de6d0a059f55ea1d53839de84c2ae96eae131b75fd00c3ee8f6678d9f6d7", size = 6869, upload-time = "2026-08-07T12:57:19.694Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/2b/989757760d725417e6131366a56f5cce88370885b6f2e064548bdc9c2cc2/django_stubs_ext-6.0.9-py3-none-any.whl", hash = "sha256:267cb4d0bc87a6c2a0edd0f0dfa59696efd3d21a86b1d346b502fe13c6ea28ef", size = 10419, upload-time = "2026-08-07T12:57:18.46Z" }, +] + +[[package]] +name = "django-stubs-ext" +version = "6.1.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "django", version = "5.2.18", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "django", version = "6.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/62/de532d58736d671b9ac0c0467909a9429758ae579d636e868ce74519566b/django_stubs_ext-6.1.2.tar.gz", hash = "sha256:2142da7fffbbe897ccecf9b4c97e09ea1a2e9291760b0a824b4c402b375ce6f9", size = 7221, upload-time = "2026-10-05T06:46:43.717Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/bb/393c3c1e24aaf37d62918ea50f3ea22e30d5f3162fc62f4c3bf3275e5ed3/django_stubs_ext-6.1.2-py3-none-any.whl", hash = "sha256:7334e687ab6dc78a6c3da90ab1ccb0e412efd827ea11de8d3f85adbad948abb1", size = 10850, upload-time = "2026-10-05T06:46:42.647Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/e1/2069291243c926a2ff1cd706c7f3eeb9b62144bf60f77c9fb9ff2fb26bd3/iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960", size = 21209, upload-time = "2026-10-06T22:48:38.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7", size = 7552, upload-time = "2026-10-06T22:48:36.959Z" }, +] + +[[package]] +name = "livereload" +source = { editable = "." } +dependencies = [ + { name = "tornado" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] + +[package.optional-dependencies] +notify = [ + { name = "pyinotify", marker = "python_full_version < '3.12'" }, +] + +[package.dev-dependencies] +dev = [ + { name = "django-stubs", version = "6.0.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "django-stubs", version = "6.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "prek" }, + { name = "pytest" }, + { name = "typing-extensions" }, +] + +[package.metadata] +requires-dist = [ + { name = "pyinotify", marker = "python_full_version < '3.12' and extra == 'notify'", specifier = ">=0.9.6" }, + { name = "tornado", specifier = ">=6.1" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'", specifier = ">=4.4" }, +] +provides-extras = ["notify"] + +[package.metadata.requires-dev] +dev = [ + { name = "django-stubs", specifier = ">=6.0.9" }, + { name = "prek", specifier = ">=0.5.5" }, + { name = "pytest", specifier = ">=8.4.2" }, + { name = "typing-extensions", specifier = ">=4.4" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pluggy" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/db/7fc19e6f2dc92a966727031389fc2e08b558f0f25eb7403c1119ad4713cd/pluggy-1.7.0.tar.gz", hash = "sha256:d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8", size = 123304, upload-time = "2026-10-15T09:50:58.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/9e/2b38731e0fc536806f16490e1a12d7f0dc2a1235aa8cc07bcc75416a7daa/pluggy-1.7.0-py3-none-any.whl", hash = "sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec", size = 27082, upload-time = "2026-10-15T09:50:56.808Z" }, +] + +[[package]] +name = "prek" +version = "0.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9b/7b/11538b8e6cd7e269261feae62a2d08818fdcf8f43c3b1a253cec01a5488d/prek-0.5.5.tar.gz", hash = "sha256:16bf596768bb754779c7d42317e9ca16ac609c0cce7d13bae0829944ab40a26a", size = 570428, upload-time = "2026-10-05T06:08:59.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/77/189e554ea92baf6c1d33733b3e9d1ed819ab7d77e57eb2e3ead0bb1d8060/prek-0.5.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:50fbceb0f728b504023f6e6c9037ac0656436949d94f7a85bac5542e6c1a4a75", size = 6038669, upload-time = "2026-10-05T06:08:39.368Z" }, + { url = "https://files.pythonhosted.org/packages/63/ea/fc857c6b3f1e3fdb4d81bdca2051076de82e986cb1455a01b351ac7a5cd1/prek-0.5.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2f610927b95e84b28173a4c810892a7b5d5bb55bee792ddf97178fa97654633e", size = 5548955, upload-time = "2026-10-05T06:08:41.428Z" }, + { url = "https://files.pythonhosted.org/packages/47/c3/2bab54f6393fb9f247225681d476086e24fa6242f329e9ca1e5bd638ec50/prek-0.5.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:120ef7d4b6bb58155e74827743c266e48501efb0e4c37ff726b14fe6e86a126b", size = 5848228, upload-time = "2026-10-05T06:08:44.051Z" }, + { url = "https://files.pythonhosted.org/packages/aa/f9/1279e1c83819de5904db1a09f4afc589932a2e62c7063713b1fe3a54ee46/prek-0.5.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2f6c453a12c21757b23bbbc97415aef78527841ad3f9e61f52cb35cf012f93e5", size = 6371296, upload-time = "2026-10-05T06:08:46.278Z" }, + { url = "https://files.pythonhosted.org/packages/93/fc/e566be5919eaffef4cc22488f16e4bbfd08961734bd67c06f85d405e4cbc/prek-0.5.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73d509c0d9556171de2502bada4962be7a25871e7b23e782549074e1416dc850", size = 6269922, upload-time = "2026-10-05T06:08:48.645Z" }, + { url = "https://files.pythonhosted.org/packages/d2/39/a48d8e6203055bff7bb734b1f2f9aede4a4539bee6ab4c9273dbc01fa546/prek-0.5.5-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:76c867fdbd4e4f19e6b6b2988bd1e1407d1324170ebcf1428e4e34f8f0751a99", size = 5861518, upload-time = "2026-10-05T06:08:50.808Z" }, + { url = "https://files.pythonhosted.org/packages/b7/03/690fba9543033e25ea2e7b2ae1cca1ae8d8c2e32295f0d66f4b50934b8c7/prek-0.5.5-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:c69f60c2b3b4d481e38d0eab3b97ac05ec05d7648ae690925ae95ca9d22c7643", size = 6383685, upload-time = "2026-10-05T06:08:52.868Z" }, + { url = "https://files.pythonhosted.org/packages/8c/04/1e9ccf3a61a3bfd4107b83973f7afde9177b6ff8f2d5d4b00125e77c2100/prek-0.5.5-py3-none-win_amd64.whl", hash = "sha256:15bff9039349466ea9bf6b0c173b822ff08f8ae4cfbfdea782492e519c06d874", size = 5784759, upload-time = "2026-10-05T06:08:55.056Z" }, + { url = "https://files.pythonhosted.org/packages/41/d9/bb6811e438d3c74d26cb5d72e6343606e0a7a6283fee87521189f4ba1191/prek-0.5.5-py3-none-win_arm64.whl", hash = "sha256:3db3191d2509830c75438e20956a47c2259867d18a4106064ed75088b968a0c6", size = 5517355, upload-time = "2026-10-05T06:08:57.193Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pyinotify" +version = "0.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/c0/fd5b18dde17c1249658521f69598f3252f11d9d7a980c5be8619970646e1/pyinotify-0.9.6.tar.gz", hash = "sha256:9c998a5d7606ca835065cdabc013ae6c66eb9ea76a00a1e3bc6e0cfe2b4f71f4", size = 60998, upload-time = "2015-06-04T15:50:23.633Z" } + +[[package]] +name = "pytest" +version = "9.1.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/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "sqlparse" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/d3/3f06a1006f2261d1342aefb3c71eed02f5d4ca5bdbecd86ebc12ad38306e/sqlparse-0.6.0.tar.gz", hash = "sha256:113c35c75365ab9cc9c7231d68c6428fb11c085fc8e9eb1ad659b7ddbf6cd2b9", size = 178477, upload-time = "2026-08-13T19:16:06.396Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/50/f00935da0ec7cbf325f8dc4f772ae46fbc7b672dd62876e73f0a94adda57/sqlparse-0.6.0-py3-none-any.whl", hash = "sha256:b861c0288ce2fa56209a9a6412d2e066ac664b3873b89c26c9d8415e8e32996f", size = 50070, upload-time = "2026-08-13T19:16:04.062Z" }, +] + +[[package]] +name = "tomli" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/78/9ad63712633ed3ab5cc1a648d863d7e7da371e9425e209555a0fe711b695/tomli-2.5.0.tar.gz", hash = "sha256:264507556cd8b8c8e7c6ee037cdf443a463f03f4c958e57195e3d369711b8ff6", size = 17662, upload-time = "2026-10-07T12:23:37.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/ab99b60ee52acd949684febabc3005d0045d0f66bebd9cdebd67372d26dd/tomli-2.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c4dc1c1781f2f716de763d1e9a7b34c6a894e167e291c7c5d16c72f7a9538545", size = 163901, upload-time = "2026-10-07T12:22:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/bc/00/ee01b7ed4579180fff07142d290257f25ba786f23f3ec6005f620933c2f5/tomli-2.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eff8babca5a7999bc137acbc7482a8b7e17ffca5075ab41f5d770ab408c7bfef", size = 163756, upload-time = "2026-10-07T12:22:16.957Z" }, + { url = "https://files.pythonhosted.org/packages/72/c2/4efebf65372f6583185f79799312109dddb61102d47e5c33dcfd1a297aca/tomli-2.5.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86665cee9c4835b7a7f1e8ec2c719b5258d4dc782887aded5a8ae7352a96843b", size = 268038, upload-time = "2026-10-07T12:22:18.135Z" }, + { url = "https://files.pythonhosted.org/packages/53/07/5850468e925d898abb36038666f9c333a94d2a223e802a8ba5b6d319d23f/tomli-2.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e369fd63331746182360977b1892bfc215476a30d61612d732425311639f56", size = 276422, upload-time = "2026-10-07T12:22:19.567Z" }, + { url = "https://files.pythonhosted.org/packages/b4/87/f293984cdcf83c054196d4fd3dad44fc68ae55b4b8c44bc76cef360c3150/tomli-2.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ad1ea345759240d6463efa0ed1c704402752e49aa21476620738d74d72d8aa1", size = 272616, upload-time = "2026-10-07T12:22:20.794Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ce/db582886b3c1219d3fec93ebd669332482e5aee7a91e0f7838d84f2d1759/tomli-2.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:96243987194634bd411066ce40c952e108f86af04db533ecd8ac3ff2a85b1885", size = 276593, upload-time = "2026-10-07T12:22:22.12Z" }, + { url = "https://files.pythonhosted.org/packages/bf/72/7619b87dea4261fc27dd7b54c4461c129c1f7d9bb7ba3aec89c797a431b8/tomli-2.5.0-cp311-cp311-win32.whl", hash = "sha256:610b27d99f28ec5f191c7064a48f3ddb179a1fe6ca73d571483ae859f57b605e", size = 101830, upload-time = "2026-10-07T12:22:23.651Z" }, + { url = "https://files.pythonhosted.org/packages/1e/74/220106da34502304b6751a2a9b8a9fbca6c3fd47e737a2e2e3da7c61c9db/tomli-2.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:c804ae44fe7b4bab5da295e4f980a1ff04670bca9d23fe0a4e887e08ebd741a8", size = 112742, upload-time = "2026-10-07T12:22:24.972Z" }, + { url = "https://files.pythonhosted.org/packages/27/99/7d9c8b41837a7773613e169504147375c157a290167aa59ad74a085f521f/tomli-2.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:cfac177ebd6236003846ea339981f71457cb6eb748f23381eb257e45092e3980", size = 109332, upload-time = "2026-10-07T12:22:26.117Z" }, + { url = "https://files.pythonhosted.org/packages/52/ed/7baa86f87493646a594de388c7c1c40a39dd0461f7e9c0359cbeefc91fe8/tomli-2.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f4a40d03fb9f63424f0979855bdeaf44dd7696b8d59501822c10ed30ba532df", size = 164854, upload-time = "2026-10-07T12:22:27.444Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b1/44c0341f2224397855723c7a8a39f718ea6fcbcc3dacc66e5aeca0f334e3/tomli-2.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9ebf8d19b17bd0daeb7b7dec81a946a439b753942fd0210d6e96c532249eea6b", size = 164074, upload-time = "2026-10-07T12:22:28.679Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e2d5b7d3fba47adedb23de616c16d428ea076c79a3d8e1d95d649ffe197e/tomli-2.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf0b5e8e0f68ebb494356e577c06c139161efd8d3b9050f93b39b7c26cc54ff0", size = 274274, upload-time = "2026-10-07T12:22:29.804Z" }, + { url = "https://files.pythonhosted.org/packages/43/90/6090e706ff27a6f89f4a40578e3324b95c3cd8c4150868aabf33a8f414c3/tomli-2.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cf74416bdc94ae458b14e37286c1073081850ac8459a00d0c5efef5d44294c6", size = 286435, upload-time = "2026-10-07T12:22:31.297Z" }, + { url = "https://files.pythonhosted.org/packages/0a/9e/a2c40768df16c408f22430afb0a73e9d7e5f79c950884954649d1146b74d/tomli-2.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:61ea1ebe1e55a34ea8199cc8dbff398d35027b82271c8ac4802fd3a1fd5b1bcc", size = 278119, upload-time = "2026-10-07T12:22:32.601Z" }, + { url = "https://files.pythonhosted.org/packages/12/25/3c0cb485b98e9cfac495629b1c93c87ccf0b72fbe9d2689fd8fe62c6d5a3/tomli-2.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed53f7e89bb04f6d9e8e7799112360b0c4d5cbff067de0814c98c37c39b920f7", size = 286177, upload-time = "2026-10-07T12:22:33.745Z" }, + { url = "https://files.pythonhosted.org/packages/77/8b/0144c65f0e37e51c18d04ae15c21b19431c165002d0131fe9aa8b0b8b1e8/tomli-2.5.0-cp312-cp312-win32.whl", hash = "sha256:e7ad033e27a516a233bea839cdb77b80146facb3b4f40bf02cd0cac165cdd5c2", size = 102760, upload-time = "2026-10-07T12:22:34.887Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/5d6d8f42fc9a05fce69354e00ff256484192f5f2fc9a2165718fa0de61ec/tomli-2.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd05de8c1698f8413dd7d869492693a0bf2211543b787ac78cd5e7536af1a6d7", size = 112722, upload-time = "2026-10-07T12:22:36.162Z" }, + { url = "https://files.pythonhosted.org/packages/30/65/df18032218db0fb9b769fb23c8039a051f15c811993995ea04c350273a32/tomli-2.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:069435bd5480429b98c5e5afb02ab21c219b6f0064680671c6dc0d46817346ea", size = 109534, upload-time = "2026-10-07T12:22:37.296Z" }, + { url = "https://files.pythonhosted.org/packages/42/e5/51736d70da209350969e15aca5c5ab6e2ce1ea87a0a892a6c13aec172a86/tomli-2.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:943276cf269e0071948d9ff697159c1735e623c1151d88abb09b74659ef0cbea", size = 163328, upload-time = "2026-10-07T12:22:38.373Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/086f80dab4ab497602644274e6dea7ec5dd0b4e262e443a8ad3bb7edee2d/tomli-2.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:463b16086865b97facd8d0b3fb4cb7c544e3f58d2a69dc3113d6db9653fdb043", size = 162246, upload-time = "2026-10-07T12:22:39.673Z" }, + { url = "https://files.pythonhosted.org/packages/aa/eb/3ecc94459f3635c92321f4e7bde571323fdb2267c50e19e3188a281eae3b/tomli-2.5.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1245a6638fc4bb0a60af38a7d45413db34a13842027c77597c712c998c62fdf0", size = 272655, upload-time = "2026-10-07T12:22:41.08Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d7/494fd1f0c37a621f1ad9975c2efadb523e8101f144ed6edb2e7fe64738f2/tomli-2.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d8bac3d603c97e6854424e5b2b5b741bdbde387e09f162fb0446812b4a8362b", size = 283595, upload-time = "2026-10-07T12:22:42.222Z" }, + { url = "https://files.pythonhosted.org/packages/70/51/bb8d62b1317e6640866f6949b2d5855e5300f2c99d46de1cd245570bba65/tomli-2.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:21e4cae4114aba25aa0d4f85cdf486d290fb35c0954d7bba536248da64d43066", size = 276253, upload-time = "2026-10-07T12:22:43.625Z" }, + { url = "https://files.pythonhosted.org/packages/66/f4/f46bd7f0763cd47de2db697dca9257c6a4adfd1a93b018cc75c8190ed5a8/tomli-2.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bbaefc84548d754be821bba7c4141c4787dda182f9e77f2f87b71213529efa7b", size = 283582, upload-time = "2026-10-07T12:22:44.983Z" }, + { url = "https://files.pythonhosted.org/packages/ac/03/70f2bcb2923a6db37818d917e124270a7f4cfd38ea576f5aa753a91c0ef5/tomli-2.5.0-cp313-cp313-win32.whl", hash = "sha256:abdbf6313b8d9efe157edeb7ab6eae4de064b1300ad31abf73755154b30abe68", size = 102628, upload-time = "2026-10-07T12:22:46.508Z" }, + { url = "https://files.pythonhosted.org/packages/dc/98/d52024bb5b0ff68b4f0d276d867f634c84a67319a7e9f6b7708a37742333/tomli-2.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd4dc129784e0c5335bd4e61dfcc4487499a013419e655cf2da1d091b7e0efdc", size = 113301, upload-time = "2026-10-07T12:22:47.647Z" }, + { url = "https://files.pythonhosted.org/packages/6f/f2/540db3a70572a8c23a28aba3e9c358ce0ffffbafc990905c1343aa265b31/tomli-2.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:69491c143d2fe063046e0301e62a810bed338fa4d1ce0fd870c27dc1e09b0d84", size = 109744, upload-time = "2026-10-07T12:22:48.925Z" }, + { url = "https://files.pythonhosted.org/packages/e4/49/caf6b307766eb9567664a8707e9d6be5fcc0e8903f18781c6677a60d80c7/tomli-2.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d3182ee2d887e507bd67319a0a61105d1dd33facc111329559a233b772c1a105", size = 162899, upload-time = "2026-10-07T12:22:50.088Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c8/68cfce773a2733a49c74f99d627fb461bd990756860099eac25617889585/tomli-2.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:521345fd1f19d45b8df87657aaa38b6f2ca3800059fadf428e7ebf479a383646", size = 162080, upload-time = "2026-10-07T12:22:51.558Z" }, + { url = "https://files.pythonhosted.org/packages/7e/b2/e5bb8651fdad593f670501a7d718b1a7f73f064d44dea15e04c04dfef45d/tomli-2.5.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e95c7614e705bfe2b04b27aa124adec59752d15813df37e2156747cab3a006b", size = 273380, upload-time = "2026-10-07T12:22:52.918Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d2/9e2d7f8b1dfe0e2b34c245986ebd55c4c553ea4ce6c47c443b332673253f/tomli-2.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ac2027d37c3afbdf4bdd377f2676f6f1d2122a5be1f1137b49dced590b37e75", size = 283228, upload-time = "2026-10-07T12:22:54.173Z" }, + { url = "https://files.pythonhosted.org/packages/ba/df/ec7b876b7b1a2718bd74a3743c076fff565b04029ba33e8f61fac262739f/tomli-2.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c414be4ed9d3cac80c42e348fa5a956117d1a48227f48026e31f59cb4a7671eb", size = 277189, upload-time = "2026-10-07T12:22:55.342Z" }, + { url = "https://files.pythonhosted.org/packages/7d/7b/e192d9eed0b9cb80da799f4d77052297fb9a2c3cc9b19f571f56ea88add6/tomli-2.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9b03d7dc168353b4132965bde20feceabaa470e570c6f59660dfae59b1f9eeb3", size = 283632, upload-time = "2026-10-07T12:22:56.735Z" }, + { url = "https://files.pythonhosted.org/packages/84/50/ff94454e75461d75623e47401ed323d65c10aab8fe9033242c20cd2fdf32/tomli-2.5.0-cp314-cp314-win32.whl", hash = "sha256:6f041843c4d3a37245c0c056fd955b186bf8b1fb85690cbe40b81230891dc34b", size = 103535, upload-time = "2026-10-07T12:22:58.084Z" }, + { url = "https://files.pythonhosted.org/packages/54/0b/bdacf05f963bd6026ebf6eeb0beda847d1d60e03e440725c64a4e08a0afd/tomli-2.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:f4b653094e18f9031102d3a1da5c729c8f222d85225b18037dac621695e46e1a", size = 114621, upload-time = "2026-10-07T12:22:59.2Z" }, + { url = "https://files.pythonhosted.org/packages/61/99/53f438fa6ae4f9d4ed0ddde3e7242b3bdc34b48c8f9948b72b9e9b127676/tomli-2.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:3f89d10c1ff6a38d992c27fc8a4816af71a909e08a40ec66934240b1e74347c3", size = 111572, upload-time = "2026-10-07T12:23:00.479Z" }, + { url = "https://files.pythonhosted.org/packages/b9/20/1f88f19427d380a40e90a770e087489eaafe4aeee070ae88ed2bbec00acd/tomli-2.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e9e15b4a6c7dd6b85b5fbab29488a73f1f70de516942308daa266bf0e0aeb0d4", size = 171814, upload-time = "2026-10-07T12:23:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d0/56/cbe5079c9f9a54b9b3e27fc82f08f3cb36edee75561679f53d2380c801d6/tomli-2.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e12bbcd32897272fb05929110362ae9ff4c1b9bb26bd9e971e71dcd3275b4c3d", size = 171324, upload-time = "2026-10-07T12:23:03.18Z" }, + { url = "https://files.pythonhosted.org/packages/2b/30/1d53fd3b0f1cb3ba542e345ec32c26aefdddc4e829e4f3429af8a4f27782/tomli-2.5.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20aa36de8f2cf87237143bc1fa1aae8d6612c09118f4da21c6a684db5dd1f6f9", size = 297441, upload-time = "2026-10-07T12:23:04.345Z" }, + { url = "https://files.pythonhosted.org/packages/66/d9/0800acb6a111686f764c1b91ef15cc42a20a66a46013bb42220f1d2c61c1/tomli-2.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22185fad8a1e622f064e78008018a0dd3323550dcb479cb7a1d296888d74024f", size = 307476, upload-time = "2026-10-07T12:23:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/e8/63/30a8f3cd51b5bec37f04744bad0b0dc6160df84aad4f27b0e9283d66f221/tomli-2.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:984012f71908165449a951de2050d52f276bfe3aa5d5f570f63ddad814370374", size = 296113, upload-time = "2026-10-07T12:23:07.202Z" }, + { url = "https://files.pythonhosted.org/packages/ab/18/0b9ffc597e69c5a1e20a7823cb60d54b39a9f54e91edcb8574f022186758/tomli-2.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f79203b3965b4000e91808aaa7c040206093f2b8bf86f455982f2274c9ccf442", size = 307725, upload-time = "2026-10-07T12:23:08.508Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c7/18f8baae0b5607a60e8e19b4a7fedee43a8ff6458e3896dcbbadeeac9c22/tomli-2.5.0-cp314-cp314t-win32.whl", hash = "sha256:91294a9fb94a75542f6e46e4a2ae709bd8d9b51134098cae5cf3bea5478b6d03", size = 108546, upload-time = "2026-10-07T12:23:09.956Z" }, + { url = "https://files.pythonhosted.org/packages/72/34/4cca9739254130627bde87500b3f2b512154fe2f278efa7e2a5e10ad4bcb/tomli-2.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f15e3e0b835a6d68b10c86bf80a3149780498d6911c93c3ffd1861d19f9200f1", size = 117814, upload-time = "2026-10-07T12:23:11.486Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fb/afa530d47dd80a78fce43beac6bc6e00f84558eafcffbc6f37b21e80d056/tomli-2.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6664b7ae7af7294256c53960a6103077f4914cec8ff98479c352f622c6f6b2f0", size = 115188, upload-time = "2026-10-07T12:23:12.728Z" }, + { url = "https://files.pythonhosted.org/packages/66/98/316fdc00f8c0939e6fe50461dd343c162d3ad51d1286eb25b7db54361d50/tomli-2.5.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a525685c2f97da40762b8695eb7aa0af4c8344ca1905c73e4e29cb04d34607dc", size = 162775, upload-time = "2026-10-07T12:23:13.941Z" }, + { url = "https://files.pythonhosted.org/packages/c5/22/7b10fa5bb01c9539f53f69b619361b19350acc73657772ea7ac70ba309a8/tomli-2.5.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:9dbb18c1cfb2f6517942fc9314437f66aa06d94436ffb1f06102ef3572f35276", size = 161406, upload-time = "2026-10-07T12:23:15.215Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e7/1a069d86dfd20f1f84f71c63faed9f83c1d890bc06c27d82dc7d888fb573/tomli-2.5.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:752e8b1aa6a4367ef8bf6a1a1e005540f7ed055ba36d7193796812ca5404eb52", size = 273855, upload-time = "2026-10-07T12:23:16.471Z" }, + { url = "https://files.pythonhosted.org/packages/ae/83/d1ef43d1687d092ab9c235455c76e6e709483b346b056f086095c7c263a5/tomli-2.5.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47300f9bf791808f77d82747691c4bb09cb14bdf3060cca99b42cdc4361d5a7", size = 284910, upload-time = "2026-10-07T12:23:18.166Z" }, + { url = "https://files.pythonhosted.org/packages/cc/05/f4d9cf7de61822ece0c3873f30d291e324911c71a378b8bfe5ced13fd9f5/tomli-2.5.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:19b0dd8749f4ea2f112c5fcfb3c5248390c899d7e2e173f1d91abee1fa0ff391", size = 277723, upload-time = "2026-10-07T12:23:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/42/28/78262493141fa543151cf005760c3cb01d09fc28a11f993c05109902cb8c/tomli-2.5.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:57b1c3b01fab802e2899bc3d168dca320e14165e2fd9fd584760fb4ca5826859", size = 285115, upload-time = "2026-10-07T12:23:20.698Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b9/e1dab9a30bcb677b5cc5cee810609cfd64f24306a3055767dd3fda00b1e0/tomli-2.5.0-cp315-cp315-win32.whl", hash = "sha256:667e521b37a6c5ccaa044202c235b530f90177ffe2cd4a64ecc213c7dd535feb", size = 103475, upload-time = "2026-10-07T12:23:21.941Z" }, + { url = "https://files.pythonhosted.org/packages/4c/bd/31a3790c11d6ea95fcf5e6022ac0f8d0543c9b61120b730fc481bd43d3b4/tomli-2.5.0-cp315-cp315-win_amd64.whl", hash = "sha256:d747252933c8a65ef6bd8da0fbb7ce28a90eb6119d8cd00772cd528aa07b68d5", size = 114589, upload-time = "2026-10-07T12:23:23.098Z" }, + { url = "https://files.pythonhosted.org/packages/47/a2/4f6310fa699364f0e3af7ee3af88dddd9af066d33e716a0265bbe2b3ea84/tomli-2.5.0-cp315-cp315-win_arm64.whl", hash = "sha256:75dbcde8751b0a960aa3de173aa5e894d590755c6d7758b7e774c06f1dc3cbdd", size = 111493, upload-time = "2026-10-07T12:23:24.233Z" }, + { url = "https://files.pythonhosted.org/packages/68/14/00853f0b396d8971107ae1921bb5b322fdee1650d2f16bf06c20adb532e5/tomli-2.5.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:2419c2a189551987b59d80e63ec355671283336f41c6b9b89462df679c7d0c57", size = 171380, upload-time = "2026-10-07T12:23:25.512Z" }, + { url = "https://files.pythonhosted.org/packages/89/ad/fa6949321dadee46b27363974fb197b94c911c3b0f7a5fd26d7dc18fc2a0/tomli-2.5.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:0dc598040da8d42cf20f0be588ed7004f46db12a0ac6c32e03a59dccedaaadcd", size = 170553, upload-time = "2026-10-07T12:23:26.855Z" }, + { url = "https://files.pythonhosted.org/packages/53/aa/3056c919eb3e084df3752b2cf5f865dcc04af0b27dba2f66d7b28af4633a/tomli-2.5.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49096930c8d886c9bbdab62d2d0d17ce823ddeea522309a190b36245d5b49e01", size = 294428, upload-time = "2026-10-07T12:23:28.132Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/faeeb5d8769ea3832021d73e892c8391eae7b4b4f8b55a789127bd8b18a9/tomli-2.5.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8ade5023067f99fe72b88accd30d0ea05a158e9e32a11f124e731ea9695313f", size = 304909, upload-time = "2026-10-07T12:23:29.381Z" }, + { url = "https://files.pythonhosted.org/packages/f6/52/f094c09e73fb654b621716d019acb5d29bdfd1be01df80c281d552bda48d/tomli-2.5.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b69564772b5c8f22ea5f498dff08cfa825045b4d4c4400529000bdf818aa3b2a", size = 293220, upload-time = "2026-10-07T12:23:30.608Z" }, + { url = "https://files.pythonhosted.org/packages/86/f5/0c30541078ca4b505ce3bd76ed931facbfec524dd018535d691d1af0a6d2/tomli-2.5.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:8ff3a2ca028c7eee0c777f9a092038d0a594a9fa04e215f929a22c329e2cb142", size = 305705, upload-time = "2026-10-07T12:23:32.181Z" }, + { url = "https://files.pythonhosted.org/packages/05/74/590e7d19d6a118fc5cc5704ff358e21d95b8573f6b9443b1519f29ca8825/tomli-2.5.0-cp315-cp315t-win32.whl", hash = "sha256:62fc1bc8eb03e3a9cadfca713d65614ed8e09d974a283295ffe3a831976b4dc5", size = 108432, upload-time = "2026-10-07T12:23:33.496Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b8/63a75cfb27a17c38550e44025d3a6e7be64516fd8608a3b75703bf37d81b/tomli-2.5.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f3fcbc57b1791fa6cbe5d8434179d51de12be1a4811469529f47f6e7487a2571", size = 117281, upload-time = "2026-10-07T12:23:34.648Z" }, + { url = "https://files.pythonhosted.org/packages/72/01/e8c1debb2173973372934c68fc8e46170ab60ef23ed4592dff4dec6e8993/tomli-2.5.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d2ba24db8a9376921b5e87b4762b9adb0f3f1deaea68f2b8b0bb2c11efb9c3e7", size = 115069, upload-time = "2026-10-07T12:23:35.77Z" }, + { url = "https://files.pythonhosted.org/packages/60/3f/3e3f8fd0919249b0200c80fbc4f9a1e70be19f9883da71dfb7f8b9ab8aca/tomli-2.5.0-py3-none-any.whl", hash = "sha256:32a7b79ac57a2e83670ce329ccf675798bc5a2094783a63676866b70503f2e2b", size = 14765, upload-time = "2026-10-07T12:23:36.875Z" }, +] + +[[package]] +name = "tornado" +version = "6.5.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/61/53d562a57b28c08eda40b258c0f975e360541943ad7c7bef897a40caafda/tornado-6.5.10.tar.gz", hash = "sha256:a6b1ccd08c04b4a06fb5aeb381be99de5ad1e5375c1785e31d78c880feb57687", size = 537910, upload-time = "2026-09-15T13:47:48.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/5b/ff5fc58fa2427c30dea74c90053f4fc5eda1e7f3833ed3ecc7147fe2b311/tornado-6.5.10-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9261783640e23258694a9ff0795df430a5a7b0a651d3dd53dd0969ad6be16da7", size = 465883, upload-time = "2026-09-15T13:47:35.463Z" }, + { url = "https://files.pythonhosted.org/packages/ad/f5/cd7be26c34a3315532f3aef5f092465da8f59c334dd439d3c14aaef16461/tornado-6.5.10-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:83e6cf438b106c6b3852d70960967bb1b70c87438050dca0981e4b9aa751a4c1", size = 464046, upload-time = "2026-09-15T13:47:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/60/33/df6d7d04854a58619f8349a51e3edb138324130a7562b0bb21f115bb940f/tornado-6.5.10-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bdf942448169e5336451d0494d7e3d81cfa726d5aa312affdc4682dd62a62f6d", size = 467096, upload-time = "2026-09-15T13:47:38.559Z" }, + { url = "https://files.pythonhosted.org/packages/29/17/cc35dff68272d685cffd8600ffafbd8067e7d05e7348d9f80caddffbbd5f/tornado-6.5.10-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69acca6501eed74582b76dbbceee2a91613f54728e3e418346000d7103101676", size = 468067, upload-time = "2026-09-15T13:47:40.085Z" }, + { url = "https://files.pythonhosted.org/packages/c3/01/6e5349b4e1a53a4b4972a6716785e1fe7407f312063c3972690af8ff301b/tornado-6.5.10-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:66aaa3f57d30c6e6becee83ff28055d5930ac724214bde99393eefda83d5e015", size = 467901, upload-time = "2026-09-15T13:47:41.576Z" }, + { url = "https://files.pythonhosted.org/packages/28/5e/b4facf94370dba006819c8d304376f8b9fbec6b935b5e51bf45823a9790b/tornado-6.5.10-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4bd192b959f9128fb99b8898148070ba4574c9589b78bce42d1851131fe85828", size = 467308, upload-time = "2026-09-15T13:47:43.145Z" }, + { url = "https://files.pythonhosted.org/packages/56/ae/047938e828cafc8eca4c908fafb6588fee944e3af39a0af9d7b602499ae5/tornado-6.5.10-cp39-abi3-win32.whl", hash = "sha256:302eb1e0e3e159314eb591920529fdea80acca92df5510a2cec5bbd4f099ec72", size = 468387, upload-time = "2026-09-15T13:47:44.556Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d4/5901517f05affd752490f6a654ba31b7474664e8dd80bd045a00c220bd88/tornado-6.5.10-cp39-abi3-win_amd64.whl", hash = "sha256:37ae8f150cecfdbf747fc4e12f5e9a97ecd8cf1d4cdb3f119e2de84b11196918", size = 468828, upload-time = "2026-09-15T13:47:45.961Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1a/fd497f3a7f7b74bb04f4b94536b5c9f80742b5d50501fd27977652ddec16/tornado-6.5.10-cp39-abi3-win_arm64.whl", hash = "sha256:ce045d3c298fddd30e89a2777f97039d1b641eb9518ac7b26a4721903539c694", size = 467847, upload-time = "2026-09-15T13:47:47.283Z" }, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20260906" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/6e/abec85b9013db5b934b0280a6dd104904d84f7bcbaab2e2f3def87ac7463/types_pyyaml-6.0.12.20260906.tar.gz", hash = "sha256:f59c1cc05010b833d2d72287bbaa72610106b28d42d89a907313117faba85212", size = 18649, upload-time = "2026-09-06T06:35:35.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/c0/fc0644b7ddcfb969e95845837143cb5173ddd6e06ee4ba5fc493cd9329b7/types_pyyaml-6.0.12.20260906-py3-none-any.whl", hash = "sha256:bca893ff0d51df5c9053137d5d0e6ccd36e939a196356f1d5c16372422f5137b", size = 21282, upload-time = "2026-09-06T06:35:34.372Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/68/f1b440335057bfce71b6e50a9d09445aa2ecbd08359a337976627b8409e7/tzdata-2026.5.tar.gz", hash = "sha256:8cc73c0a0bfca7dbfa59235d60b2eff82231dee33f53d206db1acd9173cfc0a7", size = 200404, upload-time = "2026-10-03T09:23:14.143Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/21/1e5995a1c920cce14e4bffae20c665ec10e7ed03ab25e006cd741092b718/tzdata-2026.5-py2.py3-none-any.whl", hash = "sha256:b683bd1b6659ddcd810ff02ad09ba821d4bf1065072805063eb35c49617905ac", size = 347996, upload-time = "2026-10-03T09:23:12.535Z" }, +] diff --git a/yamlfmt.yaml b/yamlfmt.yaml new file mode 100644 index 0000000..01d655a --- /dev/null +++ b/yamlfmt.yaml @@ -0,0 +1,5 @@ +formatter: + type: basic + max_line_length: 80 + scan_folded_as_literal: true + retain_line_breaks: true