From 57583ef7279ea802e69ce46ae56cb9c725f20546 Mon Sep 17 00:00:00 2001 From: Vince Veselosky Date: Tue, 17 Dec 2024 13:24:44 -0500 Subject: [PATCH 01/10] Port to commoncontent and manage with uv --- .pre-commit-config.yaml | 75 + .python-version | 1 + .vscode/extensions.json | 6 +- .vscode/settings.json | 9 +- manage.py | 1 + pyproject.toml | 82 ++ requirements-dev.in | 8 - requirements-dev.txt | 178 --- requirements.in | 18 - requirements.txt | 78 - storyville/__init__.py | 2 + storyville/apps.py | 6 + storyville/fixtures/smoketest.json | 1311 +++++++++++++++++ storyville/settings.py | 78 +- storyville/urls.py | 30 +- .../genericsite/blocks/header_masthead.html | 14 - templates/iqlovecraft/index.html | 54 +- .../storyville/blocks/header_masthead.html | 27 + .../veselosky.me/blocks/novels_card.html | 29 +- templates/veselosky.me/index.html | 25 +- tests/__init__.py | 0 tests/test_general.py | 38 + tox.ini | 13 +- uv.lock | 1106 ++++++++++++++ 24 files changed, 2789 insertions(+), 400 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 .python-version create mode 100644 pyproject.toml delete mode 100644 requirements-dev.in delete mode 100644 requirements-dev.txt delete mode 100644 requirements.in delete mode 100644 requirements.txt create mode 100644 storyville/apps.py create mode 100644 storyville/fixtures/smoketest.json delete mode 100644 templates/genericsite/blocks/header_masthead.html create mode 100644 templates/storyville/blocks/header_masthead.html create mode 100644 tests/__init__.py create mode 100644 tests/test_general.py create mode 100644 uv.lock diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..735be0d --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,75 @@ +exclude: '^docs/|/migrations/|devcontainer.json' +default_stages: [pre-commit] + +default_language_version: + python: python3.12 + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-toml + - id: check-yaml + - id: debug-statements + - id: check-case-conflict + - id: check-docstring-first + - id: check-merge-conflict + - id: check-symlinks + - id: destroyed-symlinks + - id: detect-private-key + - id: no-commit-to-branch + + - repo: https://github.com/adamchainz/django-upgrade + rev: '1.20.0' + hooks: + - id: django-upgrade + args: ['--target-version', '4.2'] + + # Run the Ruff linter. + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.5.3 + hooks: + # Linter + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + # Formatter + - id: ruff-format + + - repo: https://github.com/Riverside-Healthcare/djLint + rev: v1.34.1 + hooks: + - id: djlint-reformat-django + - id: djlint-django + + - repo: https://github.com/abravalheri/validate-pyproject + rev: v0.19 + hooks: + - id: validate-pyproject + # Optional extra validations from SchemaStore: + additional_dependencies: [ + "validate-pyproject-schema-store[all]", + "tomli", + "packaging", + "trove-classifiers", + ] + + - repo: local + hooks: + - id: check-migrations + name: Check for ungenerated migrations + language: system + pass_filenames: false + entry: .venv/bin/python manage.py makemigrations --no-input --dry-run --check + - id: validate-templates + name: Validate Django template syntax + language: system + pass_filenames: false + entry: .venv/bin/python manage.py validate_templates + +# sets up .pre-commit-ci.yaml to ensure pre-commit dependencies stay up to date +ci: + autoupdate_schedule: weekly + skip: [] + submodules: false diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..c8cfe39 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.10 diff --git a/.vscode/extensions.json b/.vscode/extensions.json index ee97aff..3023e39 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,7 @@ { - "recommendations": ["ms-python.isort", "ms-python.python", "batisteo.vscode-django"] + "recommendations": [ + "ms-python.python", + "batisteo.vscode-django", + "charliermarsh.ruff" + ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 489980b..6e74e2b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,12 +2,9 @@ "[python]": { "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.organizeImports": true + "source.organizeImports": "always" }, - "editor.defaultFormatter": "ms-python.black-formatter" + "editor.defaultFormatter": "charliermarsh.ruff" }, - "isort.importStrategy": "fromEnvironment", - "isort.args": ["--profile", "black"], - "python.analysis.autoImportCompletions": true, - "python.formatting.provider": "none" + "python.analysis.autoImportCompletions": true } diff --git a/manage.py b/manage.py index dcb7b90..b7ab5b4 100755 --- a/manage.py +++ b/manage.py @@ -1,5 +1,6 @@ #!/usr/bin/env python """Django's command-line utility for administrative tasks.""" + import os import shutil import subprocess diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ff476a4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,82 @@ +[project] +name = "storyville" +version = "0.1.0" +description = "Vince's little corner of the web" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "django >= 4.2.7, < 5.0", + "django-commoncontent >= 0.3.0", + "django-environ >= 0.10.0", + "django-extensions >= 3.2.1", + "django-rich >= 1.5.0", + "django-sitevars >= 1.0.2", + "django-tinymce >= 3.4.0", + "docutils >= 0.19", + "pillow >= 10.0.1", + "rich >= 13.3.3", + + # For production or any non-local-dev work: + "gunicorn >= 20.1.0", + "setproctitle >= 1.3.2", + + # If using celery, add these: + # celery >= 5.2.7 + # django-celery-beat >= 2.5.0 + # hiredis >= 2.2.2 + # redis >= 4.5.4 +] + +# TODO Get setuptools build working +# [build-system] +# With setuptools-scm, package data files (e.g. templates) that are tracked by git will +# be automatically detected and included. Without setuptools-scm, you would need to +# specify a file pattern in MANIFEST.in to collect them. +# requires = ["setuptools>=66.1.1", "setuptools-scm>=7.0.5", "wheel"] +# build-backend = "setuptools.build_meta" + +[tool.setuptools.dynamic.version] +attr = "storyville.__version__" + +[tool.uv] +dev-dependencies = [ + "coverage>=7.2.2", + "django-debug-toolbar>=4.0.0", + "djlint>=1.36.3", + "ipython>=8.12", +] + +[tool.djlint] +close_void_tags = true +format_css = true +format_js = true +indent = 2 +max_line_length = 119 +profile = "django" +# https://www.djlint.com/docs/linter/#rules +# D018 (Django) Internal links should use the {% url ... %} pattern. +# H006 Img tag should have height and width attributes. +# H017 Void tags should be self closing. +# H021 Inline styles should be avoided. +# H023 Do not use entity references. +# H031 Consider adding meta keywords. +# H035 Meta tags should be self closing. +# T001 Variables should be wrapped in whitespace. Ex: {{ this }} +# T002 Double quotes should be used in tags. +ignore = "D018,H006,H021,H023,H031,T002" +include = "H017,H035" + +[tool.djlint.css] +indent_size = 2 + +[tool.djlint.js] +indent_size = 2 + +[tool.ruff] +line-length = 88 +indent-width = 4 +target-version = "py310" + +[tool.ruff.lint] +select = ["E", "F", "B", "DJ"] +ignore = ["E501", "W505"] diff --git a/requirements-dev.in b/requirements-dev.in deleted file mode 100644 index f87071b..0000000 --- a/requirements-dev.in +++ /dev/null @@ -1,8 +0,0 @@ --r requirements.in -black >= 23.3.0 -coverage >= 7.2.2 -django-debug-toolbar >= 4.0.0 -ipython >= 8.12 -isort >= 5.12.0 -pip-tools >= 6.12.3 -tox >= 4.4.11 diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index f3969c2..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,178 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.10 -# by the following command: -# -# pip-compile --allow-unsafe --output-file=requirements-dev.txt requirements-dev.in -# -asgiref==3.7.2 - # via django -asttokens==2.4.1 - # via stack-data -black==23.10.1 - # via -r requirements-dev.in -build==1.0.3 - # via pip-tools -cachetools==5.3.2 - # via tox -certifi==2023.7.22 - # via requests -chardet==5.2.0 - # via tox -charset-normalizer==3.3.2 - # via requests -click==8.1.7 - # via - # black - # pip-tools -colorama==0.4.6 - # via tox -coverage==7.3.2 - # via -r requirements-dev.in -decorator==5.1.1 - # via ipython -defusedxml==0.7.1 - # via django-bootstrap-icons -distlib==0.3.7 - # via virtualenv -django==4.2.7 - # via - # -r requirements.in - # django-bootstrap-icons - # django-debug-toolbar - # django-extensions - # django-genericsite - # django-rich - # django-taggit - # easy-thumbnails -django-bootstrap-icons==0.8.6 - # via django-genericsite -django-debug-toolbar==4.2.0 - # via -r requirements-dev.in -django-environ==0.11.2 - # via -r requirements.in -django-extensions==3.2.3 - # via -r requirements.in -django-genericsite @ https://github.com/veselosky/django-genericsite/releases/download/release-0.1.4/django_genericsite-0.1.4-py3-none-any.whl - # via -r requirements.in -django-rich==1.8.0 - # via -r requirements.in -django-taggit==3.1.0 - # via django-genericsite -django-tinymce==3.4.0 - # via django-genericsite -docutils==0.20.1 - # via - # -r requirements.in - # django-genericsite -easy-thumbnails==2.8.5 - # via django-genericsite -exceptiongroup==1.1.3 - # via ipython -executing==2.0.1 - # via stack-data -filelock==3.13.1 - # via - # tox - # virtualenv -gunicorn==21.2.0 - # via -r requirements.in -idna==3.4 - # via requests -ipython==8.17.2 - # via -r requirements-dev.in -isort==5.12.0 - # via -r requirements-dev.in -jedi==0.19.1 - # via ipython -markdown-it-py==3.0.0 - # via rich -matplotlib-inline==0.1.6 - # via ipython -mdurl==0.1.2 - # via markdown-it-py -mypy-extensions==1.0.0 - # via black -packaging==23.2 - # via - # black - # build - # gunicorn - # pyproject-api - # tox -parso==0.8.3 - # via jedi -pathspec==0.11.2 - # via black -pexpect==4.8.0 - # via ipython -pillow==10.1.0 - # via - # -r requirements.in - # django-genericsite - # easy-thumbnails -pip-tools==7.3.0 - # via -r requirements-dev.in -platformdirs==3.11.0 - # via - # black - # tox - # virtualenv -pluggy==1.3.0 - # via tox -prompt-toolkit==3.0.39 - # via ipython -ptyprocess==0.7.0 - # via pexpect -pure-eval==0.2.2 - # via stack-data -pydantic==1.10.13 - # via django-genericsite -pygments==2.16.1 - # via - # ipython - # rich -pyproject-api==1.6.1 - # via tox -pyproject-hooks==1.0.0 - # via build -requests==2.31.0 - # via django-bootstrap-icons -rich==13.6.0 - # via - # -r requirements.in - # django-rich -setproctitle==1.3.3 - # via -r requirements.in -six==1.16.0 - # via asttokens -sqlparse==0.4.4 - # via - # django - # django-debug-toolbar -stack-data==0.6.3 - # via ipython -tox==4.11.3 - # via -r requirements-dev.in -traitlets==5.13.0 - # via - # ipython - # matplotlib-inline -typing-extensions==4.8.0 - # via - # asgiref - # black - # pydantic -urllib3==2.0.7 - # via requests -virtualenv==20.24.6 - # via tox -wcwidth==0.2.9 - # via prompt-toolkit -wheel==0.41.3 - # via pip-tools - -# The following packages are considered to be unsafe in a requirements file: -pip==23.3.1 - # via pip-tools -setuptools==68.2.2 - # via pip-tools diff --git a/requirements.in b/requirements.in deleted file mode 100644 index b57b07a..0000000 --- a/requirements.in +++ /dev/null @@ -1,18 +0,0 @@ -django >= 4.2.7, < 5.0 -django-environ >= 0.10.0 -django-extensions >= 3.2.1 -django-genericsite @ https://github.com/veselosky/django-genericsite/releases/download/release-0.1.4/django_genericsite-0.1.4-py3-none-any.whl -django-rich >= 1.5.0 -docutils >= 0.19 -pillow >= 10.0.1 -rich >= 13.3.3 - -# For production or any non-local-dev work: -gunicorn >= 20.1.0 -setproctitle >= 1.3.2 - -# If using celery, uncomment: -# celery >= 5.2.7 -# django-celery-beat >= 2.5.0 -# hiredis >= 2.2.2 -# redis >= 4.5.4 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index d39f0fc..0000000 --- a/requirements.txt +++ /dev/null @@ -1,78 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.10 -# by the following command: -# -# pip-compile --allow-unsafe --output-file=requirements.txt requirements.in -# -asgiref==3.7.2 - # via django -certifi==2023.7.22 - # via requests -charset-normalizer==3.3.2 - # via requests -defusedxml==0.7.1 - # via django-bootstrap-icons -django==4.2.7 - # via - # -r requirements.in - # django-bootstrap-icons - # django-extensions - # django-genericsite - # django-rich - # django-taggit - # easy-thumbnails -django-bootstrap-icons==0.8.6 - # via django-genericsite -django-environ==0.11.2 - # via -r requirements.in -django-extensions==3.2.3 - # via -r requirements.in -django-genericsite @ https://github.com/veselosky/django-genericsite/releases/download/release-0.1.4/django_genericsite-0.1.4-py3-none-any.whl - # via -r requirements.in -django-rich==1.8.0 - # via -r requirements.in -django-taggit==3.1.0 - # via django-genericsite -django-tinymce==3.4.0 - # via django-genericsite -docutils==0.20.1 - # via - # -r requirements.in - # django-genericsite -easy-thumbnails==2.8.5 - # via django-genericsite -gunicorn==21.2.0 - # via -r requirements.in -idna==3.4 - # via requests -markdown-it-py==3.0.0 - # via rich -mdurl==0.1.2 - # via markdown-it-py -packaging==23.2 - # via gunicorn -pillow==10.1.0 - # via - # -r requirements.in - # django-genericsite - # easy-thumbnails -pydantic==1.10.13 - # via django-genericsite -pygments==2.16.1 - # via rich -requests==2.31.0 - # via django-bootstrap-icons -rich==13.6.0 - # via - # -r requirements.in - # django-rich -setproctitle==1.3.3 - # via -r requirements.in -sqlparse==0.4.4 - # via django -typing-extensions==4.8.0 - # via - # asgiref - # pydantic -urllib3==2.0.7 - # via requests diff --git a/storyville/__init__.py b/storyville/__init__.py index 846b583..55dc1c2 100644 --- a/storyville/__init__.py +++ b/storyville/__init__.py @@ -7,3 +7,5 @@ except ImportError: # Ignore if you have not installed celery pass + +__version__ = "0.1.0" diff --git a/storyville/apps.py b/storyville/apps.py new file mode 100644 index 0000000..92a6cc5 --- /dev/null +++ b/storyville/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class StoryvilleConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "storyville" diff --git a/storyville/fixtures/smoketest.json b/storyville/fixtures/smoketest.json new file mode 100644 index 0000000..0b2188c --- /dev/null +++ b/storyville/fixtures/smoketest.json @@ -0,0 +1,1311 @@ +[ + { + "model": "sitevars.sitevar", + "pk": 3, + "fields": { + "site": 1, + "name": "tagline", + "value": "Author, Educator, Software Developer" + } + }, + { + "model": "sitevars.sitevar", + "pk": 5, + "fields": { + "site": 1, + "name": "custom_copyright_holder", + "value": "Vincent Veselosky" + } + }, + { + "model": "sitevars.sitevar", + "pk": 6, + "fields": { + "site": 1, + "name": "header_template", + "value": "storyville/blocks/header_masthead.html" + } + }, + { + "model": "sitevars.sitevar", + "pk": 7, + "fields": { + "site": 1, + "name": "custom_stylesheet", + "value": "veselosky.me/veselosky.css" + } + }, + { + "model": "commoncontent.image", + "pk": 1, + "fields": { + "title": "Getting Things Done Flowchart", + "description": "", + "image_file": "getting-things-done.jpg", + "width": 323, + "height": 500, + "alt_text": "Workflow diagram from Getting Things Done", + "site": 1, + "mime_type": "image/jpeg", + "custom_copyright_holder": "", + "custom_copyright_notice": "", + "upload_date": "2023-04-30T17:47:14.923Z", + "date_created": null + } + }, + { + "model": "commoncontent.image", + "pk": 2, + "fields": { + "title": "Martin Luther King Jr.", + "description": "", + "image_file": "Martin_Luther_King_Jr_NYWTS_2-e1421691902357-1038x576.jpg", + "width": 1038, + "height": 576, + "alt_text": "Martin Luther King Jr.", + "site": 1, + "mime_type": "image/jpeg", + "custom_copyright_holder": "", + "custom_copyright_notice": "", + "upload_date": "2023-04-30T17:49:53.654Z", + "date_created": null + } + }, + { + "model": "commoncontent.image", + "pk": 3, + "fields": { + "title": "Nano 2017 Recap Writing Stats Chart", + "description": "", + "image_file": "WordChar2017-11.png", + "width": 1766, + "height": 1342, + "alt_text": "Chart showing words written per month", + "site": 1, + "mime_type": "image/png", + "custom_copyright_holder": "", + "custom_copyright_notice": "", + "upload_date": "2023-04-30T18:03:40.765Z", + "date_created": null + } + }, + { + "model": "commoncontent.image", + "pk": 4, + "fields": { + "title": "Cursing Fate Cover", + "description": "", + "image_file": "cover-PC1-CursingFate.jpg", + "width": 1600, + "height": 2400, + "alt_text": "Book Cover: Cursing Fate", + "site": 1, + "mime_type": "image/jpeg", + "custom_copyright_holder": "", + "custom_copyright_notice": "", + "upload_date": "2023-04-30T18:23:21.021Z", + "date_created": null + } + }, + { + "model": "commoncontent.image", + "pk": 5, + "fields": { + "title": "Shifting Loyalties Cover", + "description": "", + "image_file": "cover-PC2-ShiftingLoyalties.jpg", + "width": 1600, + "height": 2400, + "alt_text": "Book Cover: Shifting Loyalties", + "site": 1, + "mime_type": "image/jpeg", + "custom_copyright_holder": "", + "custom_copyright_notice": "", + "upload_date": "2023-04-30T18:25:10.823Z", + "date_created": null + } + }, + { + "model": "commoncontent.image", + "pk": 6, + "fields": { + "title": "2018 Word Count Chart", + "description": "", + "image_file": "2018-summary.png", + "width": 1754, + "height": 674, + "alt_text": "Chart: 2018 Word Counts by Month", + "site": 1, + "mime_type": "image/png", + "custom_copyright_holder": "", + "custom_copyright_notice": "", + "upload_date": "2023-04-30T18:36:36.358Z", + "date_created": null + } + }, + { + "model": "commoncontent.image", + "pk": 7, + "fields": { + "title": "Storyboard for Bad Penny", + "description": "", + "image_file": "BadPennyBoard.jpg", + "width": 4006, + "height": 2506, + "alt_text": "brief scene descriptions laid out in a grid", + "site": 1, + "mime_type": "image/jpeg", + "custom_copyright_holder": "", + "custom_copyright_notice": "", + "upload_date": "2023-04-30T18:39:25.363Z", + "date_created": null + } + }, + { + "model": "commoncontent.image", + "pk": 8, + "fields": { + "title": "Summoning Courage Cover", + "description": "", + "image_file": "cover-PC3-SummoningCourage.jpg", + "width": 1600, + "height": 2400, + "alt_text": "Book Cover: Summoning Courage", + "site": 1, + "mime_type": "image/jpeg", + "custom_copyright_holder": "", + "custom_copyright_notice": "", + "upload_date": "2023-04-30T18:43:56.333Z", + "date_created": null + } + }, + { + "model": "commoncontent.section", + "pk": 1, + "fields": { + "title": "Media", + "slug": "media", + "status": "usable", + "site": 1, + "description": "", + "share_image": null, + "base_template": "", + "content_template": "", + "body": "", + "date_published": "2023-04-23T12:03:47Z", + "date_modified": null, + "expires": null, + + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US" + } + }, + { + "model": "commoncontent.section", + "pk": 2, + "fields": { + "title": "Business", + "slug": "business", + "status": "usable", + "site": 1, + "description": "", + "share_image": null, + "base_template": "", + "content_template": "", + "body": "", + "date_published": "2023-04-23T12:33:00Z", + "date_modified": null, + "expires": null, + + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US" + } + }, + { + "model": "commoncontent.section", + "pk": 3, + "fields": { + "title": "Life", + "slug": "life", + "status": "usable", + "site": 1, + "description": "", + "share_image": null, + "base_template": "", + "content_template": "", + "body": "", + "date_published": "2023-04-23T12:34:53Z", + "date_modified": null, + "expires": null, + + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US" + } + }, + { + "model": "commoncontent.section", + "pk": 4, + "fields": { + "title": "Creative Process", + "slug": "creative-process", + "status": "usable", + "site": 1, + "description": "", + "share_image": null, + "base_template": "", + "content_template": "", + "body": "", + "date_published": "2023-05-06T23:55:25Z", + "date_modified": null, + "expires": null, + + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US" + } + }, + { + "model": "commoncontent.section", + "pk": 5, + "fields": { + "title": "News", + "slug": "news", + "status": "usable", + "site": 1, + "description": "", + "share_image": null, + "base_template": "", + "content_template": "", + "body": "", + "date_published": "2023-04-30T18:21:41Z", + "date_modified": null, + "expires": null, + + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US" + } + }, + { + "model": "commoncontent.homepage", + "pk": 1, + "fields": { + "title": "Vince Veselosky: Author, Publisher, Software Developer", + "slug": "default-home-page", + "status": "usable", + "site": 1, + "description": "", + "share_image": null, + "base_template": "veselosky.me/index.html", + "content_template": "", + "body": "", + "date_published": "2023-04-23T14:00:36Z", + "date_modified": null, + "expires": null, + + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + "admin_name": "Default Home Page" + } + }, + { + "model": "commoncontent.article", + "pk": 1, + "fields": { + "title": "How Newspapers Lost the Classifieds Business (and how to get it back)", + "slug": "how-newspapers-lost-the-classifieds-business-and-h", + "status": "usable", + "site": 1, + "description": "Newspapers used to own the local classifieds business, but they failed to keep up with changing times. Here's the key business model shift that leads to success.", + "share_image": null, + "base_template": "", + "content_template": "", + "body": "

Time was, newspapers owned the local classified ads business, and it was their cash cow. Many people bought the paper just for the classifieds, and it was by far the most valuable real estate of the paper. In recent years, free Internet- based alternatives like Craig’s List decimated their business and contributed greatly to the decline of newspapers.

\r\n

In this November 2009 interview, Craig Donato, CEO of Oodle, an online classifieds startup, explains that “If You’re The Challenger, You Have To Play A Different Game”. He tells Andrew Warner how he was able to build a successful online classifieds business despite free competition, and become the classifieds provider for many local newspapers.

\r\n

Donato shows that his company achieved success through innovation based on customer needs, whereas newspapers remained complacent and failed to compete. Clearly, there is room for both free and paid providers in this industry, but the key is innovation in providing customer value. Newspapers, so long enjoying monopoly privileges in their markets, didn’t have the innovation experience they needed to compete in the newly opened market.

\r\n

Watch Mixergy’s interview with Craig Donato

", + "date_published": "2010-01-08T11:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 1, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.article", + "pk": 2, + "fields": { + "title": "The Real Value of Social Media is its Weakness", + "slug": "the-real-value-of-social-media-is-its-weakness", + "status": "usable", + "site": 1, + "description": "Those to whom we are closely connected obviously are valuable to us for personal reasons, but when it comes to economic value, it’s the casual acquaintances who add the most value.", + "share_image": null, + "base_template": "", + "content_template": "", + "body": "

Many still doubt the utility of social media. I myself was among the doubters until I was forced onto Twitter and Facebook to test the social media integration for a web site I was developing. That’s when I discovered that, although Sturgeon’s Law applies to social media as much as anything else, the small percentage of “good stuff” is exceedingly valuable.

\r\n

Case in point is this article: In a pinch, Twitter found a long shot source | By Daniel Victor. Stuck playing catch up on a story on a Sunday evening, with deadline looming, journalist Daniel Victor turned to Twitter in a last ditch search for sources. Long story short, Twitter came through for him.

\r\n

The value of social networking tools like Twitter and Facebook is not immediately obvious to some (it wasn’t to me), and may even be counter intuitive. I’ve heard complaints that it’s difficult or impossible to form deep relationships through digital media, and Luddite sentiment that we should turn back to “face time” in our relationships. I disagree with the idea that deep relationships cannot be formed online, but the real value of social media is not in the deep relationships. It’s in the weak ties.

\r\n

According to network theory, it’s the weak ties in a social network that transfer the most value (by social network here we are not talking about the online space, but connections between people however they are formed and maintained). Those to whom we are closely connected obviously are valuable to us for personal reasons, but when it comes to economic value, it’s the casual acquaintances who add the most value.

\r\n

When you are looking for a job, a client, a customer, it is the people in the periphery of your network who can connect you to valuable resources you would not otherwise have found. A former co-worker has a cousin who is looking for someone with just your skills, that sort of thing. Malcolm Gladwell called these people connectors, because they move in and out of many circles, cross-pollinating them like a (forgive the phrase) social butterfly.

\r\n

The power of social media technologies is that they grease the proverbial skids. Using these new tools, we can all become connectors. The technology enables us to manage a larger number of weak ties than Dunbar would otherwise permit, thereby increasing the size and theoretical value of our personal network. You might only “know” a hundred people in “real life”, but your network on Twitter and LinkedIn might contain 500 links or more. And while all those folks won’t come to your sister’s wedding, at least a few would be happy to connect you to a local political operator to help you with a story for your paper, as Daniel Victor discovered.

\r\n

The true value of social media is not the strong connections it enables, but the weak ones.

", + "date_published": "2010-01-12T11:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 1, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.article", + "pk": 3, + "fields": { + "title": "The Three C's of New Media: Creation, Curation, and Compilation", + "slug": "the-three-cs-of-new-media-creation-curation-and-co", + "status": "usable", + "site": 1, + "description": "Every media business is built around at least one of three key content activities: creation of content, curation of content, and compilation of data into content. Many media businesses, especially the large ones, make all three of these activities core competencies.", + "share_image": null, + "base_template": "", + "content_template": "", + "body": "

Every media business is built around at least one of three key content activities: creation of content, curation of content, and compilation of data into content. Many media businesses, especially the large ones, make all three of these activities core competencies. Which sounds most like your business?

\r\n

Creation of Content

\r\n

This is what most people think of when they think about media: writing articles or features, shooting video, recording audio. In the Internet world it also includes blogging, micro-blogging, and podcasting. The creation (and publication) of original content is often a major focus (and a major expense) for media businesses, both traditional and digital, but it’s only one piece of a larger value proposition.

\r\n

Curation of Content

\r\n

A curator’s job is to select, organize, and care for the items in a collection. The title has usually been applied to museum conservators and librarians. In the Information Age, we can all become librarians. In fact, in this age of information overload, the service of selecting and organizing collections of information is more valuable than it ever was.

\r\n

Like creation, curation of content is not new to digital media. Newspapers have always filled their pages with articles acquired from other sources, and broadcasters likewise fill out their schedules with syndicated content. Complex business relationships have grown up around these practices, and that’s a big part of the disruption the Internet is causing. Digital media enables more efficient curation capabilities and new kinds of syndication relationships. The successful digital media business will use the Internet, software, and human ingenuity to become the best information filter for its audience.

\r\n

Compilation of Data as Content

\r\n

Before the Internet, compilation and publication of large data sets was the job of specialty publishers, and except for the once ubiquitous phone book and the ever useful roadmap, such publications were expensive and difficult to acquire. In the new digital media, compiling data sets into content and distributing access to the data has become relatively inexpensive. Google, Microsoft, and Yahoo now give away access to map data and satellite imagery (and see OpenStreetMap for a project that is making map data truly free).

\r\n

One of the goals of journalists has been not just to deliver the data, but to help people make sense of data and statistics. New digital media tools enable you to collect, query, and visualize data in ways that would have been impossible, or at least prohibitively expensive, just a few years ago. This area represents perhaps the greatest opportunity for Internet-savvy entrepreneurs to leap-frog traditional media. Don’t know where to start? Take a look at Data.gov for a huge collection of public domain data waiting to be sifted and visualized.

\r\n

How Does Your Digital Media Garden Grow?

\r\n

Does your digital media business focus on one of these three areas, or dabble in all three? I would be interested to hear about (and share) what tools you are using in each of these areas. Personally, I’m dissatisfied with the curation tools I have tried so far, and for me compilation of data has meant custom programming (I’m a web developer by trade).

", + "date_published": "2010-01-15T11:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 1, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.article", + "pk": 4, + "fields": { + "title": "The Tablet Fallacy (or, Old Media is Screwed)", + "slug": "the-tablet-fallacy-or-old-media-is-screwed", + "status": "usable", + "site": 1, + "description": "For you old media geezers, here’s a hint: the problem with your business is not the form factor.", + "share_image": null, + "base_template": "", + "content_template": "", + "body": "

\"Help me, Obi-wan Tablet. You're my only hope!\" says old media. But these are not the droids they are looking for.

\r\n

There has been so much hand waving in the last month about 2010 being “the year of the tablet”, it boggles the mind. Much of the buzz has centered around the anticipated announcement of a tablet device by Apple, makers of the much-admired iPhone. However, media industry wonks are all abuzz about how the new platform will redefine newspapers, magazines, and other print products.

\r\n

At the time of this writing, the so-called iSlate is merely speculation and rumor, with any real announcement still more than a week away. I’m not going to waste space talking about a theoretical device. What concerns me is the old media heralding this concept as its salvation.

\r\n

For you old media geezers, here’s a hint: the problem with your business is not the form factor.

\r\n

Setting aside whether it is sensible to put so much emphasis on a portable device that won’t fit in your pocket, especially one that is still only a theory, riddle me this: if people won’t pay for your content on the web, why would they pay for it on a specialty device? (And if they will pay for it on the web, why would they need a specialty device?)

\r\n

I’ve been listening to lots of speculation about the revolution in content presentation that tablet devices enable. My response is two words: horse hockey! I have yet to hear a single thing about the slate that does not already apply to the web. All those capabilities for rich, interconnected content presentation? The web has had that for years, and you have largely ignored it or actively fought against it.

\r\n

No, it isn’t the presentation that media companies are excited about. It’s the idea of creating a new, closed, expensive product to replace their flagging paper-based distribution. It will be like the old days, they think, when people bought newspapers every day and we commanded premium prices for display ads because we owned all the eyeballs.

\r\n

Okay, one more time: the old days are over, media people. They are not coming back. “Like a newspaper, but digital” is not a (viable) product. If we wanted newspapers, we know where to get them. We’re sorry about your gravy train going off the tracks, but it is time for you to recognize that you have been disintermediated.

\r\n

It is time to start thinking about new ways to serve your community, and new business models to go with them. There are plenty of real needs in the marketplace that could be filled by a savvy media company, one willing and able to truly innovate rather than just repainting a burning barn.

\r\n

All this talk about tablets just underscores the fact that the media industry, and especially the newspaper industry, is stuck in twentieth century thinking (and as a result is stuck with their twentieth century business model). Tablets devices may or may not succeed as a form factor, but media businesses definitely will not succeed without some fresh thinking about what their customers need and how to provide it.

", + "date_published": "2010-01-19T11:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 1, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.article", + "pk": 5, + "fields": { + "title": "Why News Archived Behind Paywall Fails", + "slug": "why-news-archived-behind-paywall-fails", + "status": "usable", + "site": 1, + "description": "It should not come as a surprise that it is hard to find people willing to pay good money for yesterday’s news.", + "share_image": null, + "base_template": "", + "content_template": "", + "body": "

One business model for online news that has been suggested, tried, and failed, is to make the news free for some short time, and then archive it behind a pay-wall. There is more than one reason why this doesn’t work as a business model, but the most obvious one is an old adage that should have been well known in the newspaper industry: yesterday’s news wraps today’s fish.

\r\n

It should not come as a surprise that it is hard to find people willing to pay good money for yesterday’s news, especially in the age of 24 hour TV news and instant digital dissemination over the Internet. Old news does have value to historians and researchers, but only after it has faded from the collective memory, decades after the fact. In the space between breaking news and historical research, the value of that content becomes nearly impossible to extract.

\r\n

Placing news articles behind the pay-wall also fails for another reason: it breaks the web. Rather, it is counter to the way the web works.

\r\n

Web pages accumulate links, which are crawled by search engines, raising the relative “value” of that page in search algorithms. The more links you have to your content, the more likely that your content will get significant traffic from search results. Content hidden behind a pay-wall cannot easily accumulate links, and visitors from search engines may find only a barrier page demanding payment rather than the content they were actually searching for. The chance of that visitor reaching for his credit card is much lower than the chance that she will hit the Back button and move on to the next (free) site in the search results.

\r\n

Certainly there are successful business models where people pay for access to information. Online news archives, however, are more valuable, both to their creators and their consumers, when they are visible to search engines and available at no charge.

", + "date_published": "2010-01-22T11:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 1, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.article", + "pk": 6, + "fields": { + "title": "NYT’s Freemium Paywall Plan is (maybe) Good Business", + "slug": "nyts-freemium-paywall-plan-is-maybe-good-business", + "status": "usable", + "site": 1, + "description": "What the Times is actually proposing is a Freemium model: you get limited access for free, or you pay a fixed fee for unlimited access.", + "share_image": null, + "base_template": "", + "content_template": "", + "body": "

Last week, the New York Times announced plans to start charging certain readers for access to their web site. Reaction was predictable: Jeff Jarvis complainedTechCrunch ran some numbers, and Mashable used it as an excuse to talk about a rumored but still unannounced Apple product (seriously Mashable? come on).

\r\n

\"The

\r\n

The first thing to understand about the announced pay model is that it is misnamed. The NYT press release refers to it as a “metered model” and most reporters are using that language. However, this phrase gives entirely the wrong impression. Most people think of “metered” as the power utility model: you pay for it all, and the more you use, the more you pay.

\r\n

What the Times is actually proposing is a Freemium model: you get limited access for free, or you pay a fixed fee for unlimited access. The only thing they are “metering” (actually measuring) is how many free page views you have used. When you hit a certain number, they ask you to subscribe. (And if you already subscribe to the Times, even just the Sunday edition, you’re covered.)

\r\n

The Times people have put some good thinking into this, and for once the business model does make some sense, despite what Jeff Jarvis says. The logic goes like this. People who use our product frequently are likely to think it is valuable, and are therefore more likely to be willing to pay for that value. Infrequent visitors, who value our product less, can be monetized with lower value ad impressions, but we don’t want to spend too much supplying these freeloaders, so we’ll cut them off at some point.

\r\n

PaidContent said it best:

\r\n
\r\n

It’s about tweaking the dials, up and down, to capture the payments of truly loyal readers who find continuing value in the brand, while not losing a critical number of occasional visitors.

\r\n
\r\n

As TechCrunch pointed out, the key to making the model work is to have excellent audience analytics. The Times will have to find the sweet spot in both number of free views and cost of subscription to maximize subscription revenue without sacrificing display ad rates.

\r\n

But more than that, the company needs to use the subscription as just one of many touch points to build a stronger relationship with loyal readers. It’s that relationship from which the real value of the business is derived.

", + "date_published": "2010-01-25T11:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 1, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.article", + "pk": 7, + "fields": { + "title": "Content Is Not Your Product: Why Newspapers Fail", + "slug": "content-is-not-your-product-why-newspapers-fail", + "status": "usable", + "site": 1, + "description": "I hear the same sentiment from executives all over the media industry, and especially from newspapers. “We deserve to get paid for our content.”\r\n\r\nDear Media Executives: You’re doing it wrong!", + "share_image": null, + "base_template": "", + "content_template": "", + "body": "

Your newspaper is Seth Godin's Meatball Sundae — full of valuable stuff, but not a product anyone wants to buy.

\r\n

I hear the same sentiment from executives all over the media industry, and especially from newspapers. “We deserve to get paid for our content.”

\r\n

Dear Media Executives: You’re doing it wrong!

\r\n

\r\n

The reason so many newspapers are sinking, shrinking, or stinking is that they have totally forgotten what the source of value for their business really is. Somehow they got confused by the 20th century mass production model and deluded themselves into thinking of content as a product that they package and sell.

\r\n

The business of media has never been about production of anything. Not newspapers. Not radio waves. Not content. The business of media is about connecting people. It’s about building relationships.

\r\n

Content is just a tool we use to build relationships.

\r\n

When you treat content as a product, your focus is placed on the transaction, and the relationship gets ignored. A transaction, in isolation, does nothing to establish a relationship, to build trust. That trust, that relationship, is what makes the attention of your readers (or viewers or listeners) so valuable to advertisers. Without it, you are just a commodity, and business will go to the lowest bidder. This is the reason that ads are becoming less effective and CPMs are dropping.

\r\n

The reason we produce content is to establish that trusting relationship with our community by providing them with something valuable for free. I intentionally use the word “community” and not “audience” here, because audience implies that we talk and they listen. In the 21st century more than ever, the relationship we as media companies have with our community has to go both ways. We must also listen and help others be heard. These activities are essential to establish a trusting relationship.

\r\n

Many newspaper executives are up in arms about falling subscription revenues, and want to replace that revenue by charging for content online. But they are missing the point. Subscription revenue was never an end in itself, it was a proxy for measuring the value of the relationships we had created. Newspapers don’t necessarily need to replace that revenue, but they do need a new way to measure the value of the relationships they maintain. I don’t think charging for access to web sites serves that goal.

\r\n

The language we use around advertising is broken as well. We talk about selling inventory and picking up remnants, as if communication were a physical good. But what we are really doing is facilitating conversations between businesses in our community and their (potential) customers. The conversations we enable create value for the whole community, some of which we capture as revenue.

\r\n

The mass market, mass media mindset has run rampant over our community institutions. Our local media (newspaper, TV, and radio alike), now seem to be more concerned with attracting eyeballs than with helping people. Eyeballs have very little value. People have uncountable value. And when people are being treated as a commodity, they notice. They don’t like it. They take their eyeballs elsewhere.

\r\n

As media, we talk, we listen, and we facilitate conversations. These three activities taken together are the essence of a media business. Take heed, old media. Talking is not a product, no matter how valuable it is what you are saying. If all your business does is talk, you will fail. If you want to succeed, then establish trust and build relationships in your community.

", + "date_published": "2010-01-30T11:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 1, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.article", + "pk": 8, + "fields": { + "title": "Journalism is too important to be locked behind a paywall", + "slug": "journalism-is-too-important-to-be-locked-behind-a", + "status": "usable", + "site": 1, + "description": "The reason journalism should be free is that journalism is extremely valuable. Sound counter-intuitive? Not at all.", + "share_image": null, + "base_template": "", + "content_template": "", + "body": "

When I hear newspaper industry veterans talk about getting paid for content, it makes me want to cry. Case in point, this speech from Bill Monroe to the Midwest Newspaper Summit in Des Moines, Iowa, given Feb. 4, 2010.

\r\n
\r\n

What’s missing in today’s marketplace is a way to enable newspapers to protect that content and to profit when others reuse it. – Bill Monroe

\r\n
\r\n

I’m sorry Mr. Monroe, but I must disagree quite strongly. The reason journalism should be free is that journalism is extremely valuable. Sound counter-intuitive? Not at all.

\r\n

The process of journalism boils down to this: it is discovering information that few people have, but lots of people need, and disseminating that information as broadly as possible to ensure that those who need it, have it.

\r\n

In a democratic society, there is possibly no process more valuable than that of journalism, outside of the process of democracy itself. From the neighborhood association to the national government, the public need to be informed about what is being done in their name, so that they can make informed decisions about the who and how of government.

\r\n

And here is the catch. The output of the journalistic process is only valuable when it is delivered in a timely manner to the widest audience. Journalism that is hidden and inaccessible is oxymoronic. Placing news behind a pay wall makes it less valuable.

\r\n

If we are to take journalism seriously as a public service, then our goal must be to spread the information farther and faster. It is ironic that the Internet has made the process of journalism more efficient and more effective, yet has made the process of supporting a business around it more difficult. But this is the reality of the 21st century, and denying this by attempting to restrict access to the news is a recipe for failure.

\r\n

If you are a big shot in the newspaper industry, I beg you, do not destroy journalism in the name of saving it. The nation needs you to be smarter and more innovative than that. Find new business opportunities in making markets more efficient, rather than creating artificial scarcity and friction. Please.

\r\n

Journalism is just too important to be locked behind a pay wall.

", + "date_published": "2010-06-13T10:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 1, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.article", + "pk": 9, + "fields": { + "title": "Efficiency: Enemy of Innovation?", + "slug": "efficiency-enemy-of-innovation", + "status": "usable", + "site": 1, + "description": "In established processes like manufacturing, efficiency creates value. In exploratory processes like innovation, efficiency destroys value. Use efficiency to generate spare capacity from your established processes. Then, use that capacity to tackle big problems.", + "share_image": null, + "base_template": "", + "content_template": "", + "body": "

The science of management in the industrial age was all about efficiency. It had to be. The whole concept of capitalism is based on efficiency. An entrepreneur acquires capital at a cost, and that capital must be made to produce profit at a rate higher than the cost of capital. If you borrowed money at 10% to start your business, you had to make it earn 11% at least. That meant controlling costs ruthlessly and milking every bit of productivity from every penny's worth of capital.

\r\n

But talk to a systems administrator about efficiency. She'll tell you that, in terms of percentage of server utilization, there are two numbers you never want to approach, numbers that will cause midnight pages and pale-faced panic. The first number, of course, is 0%. Everything is down! The second, more surprising but equally frightening number is: 100%! At 100% utilization, everything breaks, because you have no more capacity for work.

\r\n

Now a capitalist might look at a well-run data center, and his first instinct is, \"Look at all this waste! Half these servers are sitting idle most of the day.\" But the clever sysadmin will tell him that spare capacity is what keeps the data center running. If your capacity is 100 requests per second, a 101st request can bring the whole system to a screeching halt. 100% and 0% are equally disastrous. If you want your Internet business to operate, you must have spare capacity.

\r\n

Now, I'm not arguing that efficiency is somehow evil. If you are in a capital intensive business today, you still need to use that capital efficiently. Of course the capitalist theory goes that capital + labor = profit, but what often gets lost in the quest for efficiency is the fact that people are not labor. That's a false assumption, and that formula was never correct (which should not surprise anyone given its source). It's not labor that turns idle capital into profit; it's creativity and its more productive sister, innovation.

\r\n

In order to innovate, in order to create, you need some very special ingredients. First, you need people. Smart people, with a desire to solve problems, the ambition to tackle big ones, and the hubris to believe that they can do something better than everyone who has come before them. These people then need time to analyze the problem and devise or improvise solutions, and they need resources (read: money) to test those solutions.

\r\n

So no, efficiency is not necessarily the enemy of innovation. Saving time and money on existing processes creates spare capacity that can be allocated to innovation. The extra people, time, and money that are not being used to operate the existing business, instead can be applied to solve the next big problem and give birth to new lines of business. But too many business leaders still see people as labor which, if not making capital productive, they label as \"waste\". Spare capacity is inefficiency in their eyes. They see the tools of innovation as inefficiency, and so they attempt to eliminate it. With the result that they eventually become irrelevant because the industry has passed them by.

\r\n

Don't fall into this trap at your company. In established processes like manufacturing, efficiency creates value. In exploratory processes like innovation, efficiency destroys value. Use efficiency to generate spare capacity from your established processes. Then, use that capacity to tackle big problems. To stay relevant and keep growing, accept that creativity is inefficient, and pay the cost to gain the future rewards.

", + "date_published": "2012-01-19T11:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 2, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.article", + "pk": 10, + "fields": { + "title": "Natural Laws", + "slug": "natural-laws", + "status": "usable", + "site": 1, + "description": "I figure any phrase that people deem to be a \"law\" and find important enough to attribute to a specific person (even if incorrectly) probably contains some real wisdom. Here's a collection of Eponymous Laws from Wikipedia, all of which I have found to be true in my own experience.", + "share_image": null, + "base_template": "", + "content_template": "", + "body": "

I figure any phrase that people deem to be a \"law\" and find important enough to attribute to a specific person (even if incorrectly) probably contains some real wisdom. Here's a collection of Eponymous Laws from Wikipedia, all of which I have found to be true in my own experience.

\r\n

Amara's Law: We tend to overestimate the effect of a technology in the short run and underestimate the effect in the long run.

\r\n

Conway's Law: Any organization that designs a system will inevitably produce a design whose structure is a copy of the organization's communication structure.

\r\n

Gall's Law:

\r\n

A complex system that works is invariably found to have evolved from a simple system that worked. The inverse proposition also appears to be true: A complex system designed from scratch never works and cannot be made to work. You have to start over, beginning with a working simple system.

\r\n

Parkinson's law: Work expands so as to fill the time available for its completion.

\r\n

Law of the Instrument or Maslow's Golden Hammer: It is tempting, if the only tool you have is a hammer, to treat everything as if it were a nail.

\r\n

Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law. (I've also heard this restated as \"Every task takes longer and costs more than originally estimated.\")

\r\n

Occam's razor – \"Entia non sunt multiplicanda praeter necessitatem.\" Literally, entities are not to be multiplied without necessity. When two explanations are offered for a phenomenon, the simplest full explanation is preferable. (Or in modern terms: Keep It Simple, Stupid!)

\r\n

Pareto principle – 80% of consequences stem from 20% of the causes.

\r\n

Schneier's law – Any person can invent a security system so clever that she or he can't think of how to break it.

\r\n

Sturgeon's law – Ninety percent of everything is crap.

", + "date_published": "2012-01-24T11:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 3, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.article", + "pk": 11, + "fields": { + "title": "Systems and Mental Deficiencies", + "slug": "systems-and-mental-deficiencies", + "status": "usable", + "site": 1, + "description": "On mindfulness and the following of rules to make a better life.", + "share_image": null, + "base_template": "", + "content_template": "", + "body": "

I was surprised when I read some of the things writer Terry Pratchett wrote or said about developing PCA, a form of dementia. I cannot now find the original source that I read, but there are several similar articles. He described some symptoms of the disease slowly robbing him of his own mind. The inability to see certain objects when they are right in front of you. Walking into a room but having no memory of why you went there in the first place. Difficulty comprehending written text despite recognizing every letter and word. Difficulty recognizing people's faces.

\r\n

I was surprised when I read this, because these \"symptoms\" have affected me, well, pretty much my whole life. I thought they were normal.

\r\n

Since I was a child, I have had these problems. Until I have met people many times, I may have difficulty recognizing or remembering them. Sometimes I put something down, and then simply cannot find it again, despite the fact that neither I nor the object have moved at all. I stopped reading books on paper years ago because I just couldn't manage to read one through; I would get distracted in the middle of a paragraph and forget what I was reading. And as for walking into a room and forgetting why you're there? Hardly a day passes without such an event in my life. Sometimes I return to the room two or three times before I manage to complete the task I set out to do. Sometimes I never remember what I was planning to do.

\r\n

Part of my obsession with systems is the result of this bizarre array of mental quirks that I have slowly realized are not entirely \"normal\" (whatever that means). Systems are a simple set of rules that I can keep in my head. Lapses in memory become less important when the system is in operation.

\r\n

Ever lost your car keys? It's bad enough when you can't remember where you left them. It's doubly bad when you can be staring right at them and not see them. (My family calls this quirk \"object-blindness\" and it drives them crazy. \"Why did you put away every dish in the kitchen except that one?!\" I didn't see it!)

\r\n

I no longer have to remember where I left my car keys; the rule says that keys are by the door. If I forget where I left them, I can remember the rule. If I notice them sitting somewhere else (which is rare), I move them into compliance with the rule. When I'm about to put them down, I remember the rule, and I put them by the door. One consistent rule to be applied in all situations. A rule that doesn't even have to be remembered, because it can be derived again and again. (What's the best place to leave the keys? What's the first place I could put them down after entering the house?)

\r\n

I live my life by rules like this. I have a system for everything I do. A system for packing my laptop bag to be sure I don't forget anything. A system for loading the dishwasher. A system for making breakfast. It sounds ridiculous, but I have rules for all these things so that I don't have to rely on my memory to get them right.

\r\n

The most important element of my life systems is an attitude I learned only in the most recent quarter of my life, a skill Buddhists describe as mindfulness. Mindfulness means being fully present, in the moment. It releases you from remembering the past or worrying about the future and focuses your attention on the present, the here and the now. Mindfulness is what enables me to obey the rules now, as I perform the actions which later may confound me. It is what allows me to think when I put my keys down and observe the rule of where to place them.

\r\n

By knowingly placing my keys by the door where they belong, I ensure that later I can find them again. By paying attention to my actions in the present, I ensure success in the future.

", + "date_published": "2012-04-29T10:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 3, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.article", + "pk": 12, + "fields": { + "title": "A Framework for Innovation", + "slug": "a-framework-for-innovation", + "status": "usable", + "site": 1, + "description": "How does a large company create an environment that encourages and leverages internal innovation? Here is my checklist of prerequisites for \"enterprise\" innovation.", + "share_image": null, + "base_template": "", + "content_template": "", + "body": "

How does a large company create an environment that encourages and leverages internal innovation? Here is my checklist of prerequisites for \"enterprise\" innovation:

\r\n

Great people. You may think this goes without saying, but it cannot be emphasized enough. You cannot hire drones who put in 8 hours for a paycheck and then head out the door. You need passionate, creative people, people who love their work, people who are impatient with \"getting by\" and want to be the best at what they do. These are the Innovators. Without them, innovation does not happen.

\r\n

A clear vision. Innovation happens at the edges. It is not a top-down directed process, it is an organic, bottom-up growth. In order for the innovators at the edge to produce innovations that are relavent to the business, top management must articulate and communicate a clear vision for the direction of the company. If the innovators can see the direction, they will innovate in that direction and get you there faster. If not, they will innovate in random directions, and you won't get the full benefit of innovation. A clear vision is the difference between innovation and distraction.

\r\n

Spare capacity. Innovation is experimentation. Innovators need time to experiment, and they won't have that if 100% of their time is allocated to executing your current plan. This is the hardest thing for top managers to accept, but it is absolutely essential. You need slack time, or there simply will not be any innovation. Allocate one slice of your capacity for executing the plan. Reserve a second slice for unplanned work and process improvements. Allocate a third slice explicitly to innovation. The relative size of the slices will be entirely dependent on your own business and your desired outcome. My personal preference is 50/30/20.

\r\n

Freedom to make decisions. Innovators by definition have to make decisions, make changes, form partnerships, and allocate resources from the pool of spare capacity. If permission is required to accomplish these things, then innovation will be quashed before it can succeed.

\r\n

Accessible Business Intelligence. If you are going to give innovators permission to make decisions, you must give them the information and tools they need to fuel decision making. Innovators need transparent access to customer data, product data, sales data, cost data. Without it, they are shooting in the dark, and the chances of success are low. Innovators also need easy access to tools for gathering their own data, for evaluating experiments and measuring success vs. failure.

\r\n

Freedom to fail. Innovation is experimentation, and experiments, by their nature, do not always have the expected outcome. When Innovators exercise their power to make decisions, some of the decisions will be wrong ones. Innovators need to feel secure that they will not be punished for taking a chance if it doesn't work out. Remember, these folks are corporate employees, not risk-taking entrepreneurs. They don't stand to make millions if their innovation succeeds, so they shouldn't have to give up their health coverage and pension if it doesn't. Make it clear that failure is a learning opportunity, not a firing offense.

\r\n

The above are a few requirements for fostering innovation in large companies. Ultimately, innovation only happens where the culture supports it. Managers at all levels build company culture through their hiring and firing practices first, and management styles second. If your managers fear the new and different, your culture will never innovate. Ensuring the above factors at all levels of the organization should help to unchain your hidden innovation potential.

\r\n

What's missing from this list? How does your company encourage (or discourage) innovation?

", + "date_published": "2012-05-14T10:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 2, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.article", + "pk": 13, + "fields": { + "title": "How to set up a new PC in 12 Steps, or How I spent my evening renewing my disgust with Windows", + "slug": "how-to-set-up-a-new-pc-in-12-steps-or-how-i-spent", + "status": "usable", + "site": 1, + "description": "On the pain if setting up a new PC.", + "share_image": null, + "base_template": "", + "content_template": "", + "body": "

Step 1: Spend 30 minutes unpacking boxes, peeling plastic, and connecting cables.

\r\n

Step 2: In breathless anticipation, press the power button.

\r\n

Step 3: Spend another 30 minutes hunting for the Windows Product Key so you can access the computer you just bought. Find it, finally, on an indelible sticker on the far side of the computer's case.

\r\n

Step 4: Enlist an assistant to type the Windows Product Key while you hang upside down under the desk using a flashlight to read it out.

\r\n

Step 5: Insert CD to install hardware drivers, because Windows does not know how to use the network card in your PC.  Try to convince Windows that you know what you are doing and yes, you really want to run that program from the CD.

\r\n

Step 5b (optional): Wonder at how Windows has not only failed to improve, but has actually gotten worse in the 10 years since you last bought a PC.

\r\n

Step 6: Using a clunky-looking \"wizard\" from the CD, attempt to connect to wireless network. Be unable to find your wireless access point in the list because you live in a crowded apartment building, and the list is sorted randomly rather than by signal strength or even alphabetically. Notice that the list has multiple pages, and advance to page two. There it is.

\r\n

Step 7: Enter password for wireless access point. Curse in frustration when it fails to connect. Blush with embarrassment when you realize CAPSLOCK is on. Turn CAPSLOCK off and try again.

\r\n

Step 7b (optional): Curse the inventor of the CAPSLOCK key.

\r\n

Step 8: Start Internet Explorer. Type \"google.com/chrome\" into the location bar to download a real browser. Try to convince Windows that you know what you are doing and yes, you really want to run that program.

\r\n

Step 9: Sign into Google Chrome. All extensions and bookmarks are automatically synced. Awesome.

\r\n

Step 10: Using Google Chrome, visit www.ubuntu.com and download the Windows Installer to install a real operating system. Try to convince Windows that you know what you are doing and yes, you really want to run that program.

\r\n

Step 11: Let the installer reboot into Linux. Be amazed at how all the hardware is recognized immediately, including the wireless card. Feel like Ubuntu just gave you a warm hug when the wireless network manager pops up on the screen and offers to connect you to your very own wireless access point if you will be so kind as to enter the password. Check to ensure CAPSLOCK is off. Enter password.

\r\n

Step 12: Click \"Install Updates\" when the update manager offers to do so. Wait.

\r\n

Step 12a (optional): Write a blog post about your experience while waiting for updates to download. Feel sorry for people who have not yet discovered Linux.

", + "date_published": "2012-10-10T10:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 3, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.article", + "pk": 14, + "fields": { + "title": "Getting Things Done -- Productivity System", + "slug": "getting-things-done-productivity-system", + "status": "usable", + "site": 1, + "description": "Applying the Getting Things Done system to a digital lifestyle.", + "share_image": 1, + "base_template": "", + "content_template": "commoncontent/blocks/article_featuredimage.html", + "body": "

David Allen's Getting Things Done: The Art of Stress-Free Productivity is a phenomenon in the tech community. If you're reading this blog, you've probably already read the book, or at least know something about the productivity system that it defines. I read it years ago, but like many readers never put into practice more than a tiny portion of the system.

\r\n

As 2012 drew to a close and I looked back on all the things I meant to accomplish, I decided that I should give this productivity bible another look, in the hopes of getting more things done in 2013. I won't bother to summarize the system that David Allen defines. The book is very readable and does a much better job than I could. Instead, I'm just going to note how I decided to apply the principles of his system in my own life, especially given the changes in technology and lifestyle since the book was originally published a dozen years ago.

\r\n

Some essential elements of the GTD system include:

\r\n\r\n

The original GTD system was developed in a paper-focused world, before cloud-based calendars and Internet-connected phones became the norm. I've worked really hard to eliminate the mountains of paper in my life, so I have no interest in buying filing cabinets and manila folders.

\r\n

The vast majority of things I need to manage in my own life are actually digital. Email, digital music, more email, PDF downloads, email again, digital pictures, and did I mention the email? Digital references take up far less space, are easier to move, and are full-text searchable. I quickly resolved that my organization system would be digital, not physical.

\r\n

As I had already begun to use it for scribbling notes I wanted to keep track of, I decided to do the simplest digital thing that could work for staying organized, and elected Evernote as my default tool.

\r\n

Evernote gives me myriad advantages over a paper-based system.

\r\n\r\n

Setting up Evernote to work with my system was dead simple. I renamed the default notebook to \"INBOX.\" Any random notes I capture are there, waiting to be processed when next I process my inboxes. I created a Projects stack, containing a notebook for each large project, and a \"Miscellaneous\" notebook with a separate note for each smallish project. A Reference stack contains various notebooks organized by topic where I can file informational notes, PDF or Word documents, photos of the whiteboard scribbles from a brainstorming session, or any other assets I need to keep around.

\r\n

With a physical inbox, Allen recommends dealing with things that won't fit into it by writing a reminder of them on a piece of paper and placing the paper in the inbox. Since my inbox is digital, physical things won't fit into it. So if I need a reminder of a physical thing, I snap a picture of it with my phone and add it to my Evernote inbox. I also file away papers by scanning them or taking a picture with my phone (and then trashing/recycling the physical paper).

\r\n

Allen recommends keeping a list with all your \"next actions\" on it. I have become accustomed to visualizing work using a kanban system, so instead of a \"next actions\" list, I have a notebook called Backlog containing a note for each task, and another called WIP that contains notes for the tasks I am currently working on. When completed, I move them to the Done notebook. During my weekly review of open projects, I determine the next actions for each project and add a note for each one to the Backlog.

\r\n

GTD recommends keeping an agenda list for every regular meeting you have, so that you never have to be embarrassed that you forgot to ask Bob about that one thing when you spoke with him this morning. I keep an Agenda notebook in Evernote, with a separate note for each person or group I speak to regularly. If I run into someone in the hallway, I can whip out my phone and access their agenda immediately. Any notes generated from the meeting go back into the inbox to be processed.

\r\n

Since most of my reading is also digital, I use Pocket (formerly Read It Later) as my Reading list. I do a lot of my reading in the moment as a less-than-two-minute task, but when I need to queue something up, I toss it into Pocket. I am finding, however, that my appetite for reading later is a bit more ambitious than the amount of time \"later\" actually affords me. Perhaps I need to work on this.

\r\n

GTD Patterns I Don't Apply

\r\n

GTD recommends keeping your Next Action list organized by Context: Things you can do at home, at work, at the phone, etc. I found organizing by context to be almost useless, because almost all my tasks can be performed in any context. My work is all digital, and my work computer is a laptop I bring home with me at night. In a pinch, most of my work could performed on my phone. I always have a phone in my pocket, so there's no need for a \"calls\" context, I can make calls from anywhere. Most of my home activities are habits rather than tasks (take out trash, wash clothes, etc.) and therefore do not need to be tracked.

\r\n

I don't have a \"Waiting For\" notebook for tracking delegated tasks. Instead, I place a reminder on my calendar to follow up on a certain date if the awaited item has not arrived in my inbox by then. I also make \"appointments\" blocking out time to complete important tasks, otherwise there is a risk that my schedule will fill up and leave no time, or that I will get distracted by the in-the-moment work, leaving important things too late.

\r\n

My calendar is already digital and synchronized across my devices. I use the Exchange calendar provided by my company, but I could just as easily use a synchronized iCloud or Google Calendar.

\r\n

Finally, I decided that the tickler file was really an artifact of the paper world where a calendar is a sheet of paper with little boxes drawn on it. You can't file papers in those little boxes, so you need those 43 folders to store date-specific items. In my all-digital world, if I need to be reminded of something on a certain date, I can just drop it onto my digital calendar and store it there, or at worst store a link to some other repository. So I don't have a tickler notebook in Evernote, instead I use my calendar directly to fill that role.

\r\n

I'm just getting started using and tweaking this system, and I'm sure it will evolve over time. Perhaps I will write a follow-up post in a few months to record how it has changed and how effective it has been.

", + "date_published": "2013-01-13T11:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 3, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.article", + "pk": 15, + "fields": { + "title": "Systems vs Habits: Why GTD Often Fails", + "slug": "systems-vs-habits-why-gtd-often-fails", + "status": "usable", + "site": 1, + "description": "Good habits are a way to automate your behavior the way irrigation ditches automate watering. They allow you to accomplish work without effort. But if you don't have them already, good habits can be hard to form.", + "share_image": null, + "base_template": "", + "content_template": "", + "body": "

In my previous post, I wrote about David Allen's Getting Things Done book and productivity system. If GTD has a weakness, it is that, although the book describes the system very well, it does a poor job of describing the change of daily habits you'll have to perform if you really want to implement the system. The major reason people fail at implementing a GTD-style productivity system in their lives is that, no matter how simple the system may be, it's a big change from what they are used to.

\r\n

Leo Babauta is a self-made expert in changing and forming habits. His Zen Habits blog has changed the lives of many of its readers. So when I decided to try getting organized once again, there were two books on my reading list: David Allen's (the System), and Leo Babauta's Zen To Done, Leo's personal take on productivity.

\r\n

On Habits and Willpower

\r\n

I like to think of habits as irrigation ditches.

\r\n

If you are a farmer who wants your fields watered, the obvious thing to do is to go get some water. But carrying buckets of water from the well to your field is inefficient and places a low upper limit on the amount of crop you can grow effectively. The effective farmer instead spends his effort digging irrigation ditches. It's exhausting work, and at first it seems to generate no benefit at all. But once the ditch is complete, the water flows naturally into your fields on its own, without effort.

\r\n

Good habits are a way to automate your behavior the way irrigation ditches automate watering. They allow you to accomplish work without effort. But if you don't have them already, good habits can be hard to form.

\r\n

As humans, we have a natural aversion to change. The world and activities that we are comfortable with got us this far, so they must be good, right? Change might make things worse. So if your bar for success is mere survival, aversion to change is probably a good thing. That's why change makes us uncomfortable. It's instinctive.

\r\n

Each of us has a limited ability to tolerate change. Too much change makes us too uncomfortable, and we start to squirm, trying to avoid the change, to get back in our comfort zone. The uncomfortable feeling we get from too much change we call \"stress\". When we're trying to affect change, we call the ability to tolerate it \"willpower\". But this is misleading, because willpower must also be expended to tolerate change that comes from the outside, change that we don't want.

\r\n

Remember Steve McCroskey from Airplane!, the guy who picked the wrong week to quit smoking? Too much change, he ran out of willpower.

\r\n

Habits are a way to acclimate yourself to a new condition or activity, so that you stop seeing it as stressful change and start seeing it as normal.

\r\n

Habits and Productivity

\r\n

GTD asks you to master five classes of activity:

\r\n\r\n

But the GTD system itself doesn't tell you how to master these activities. For most people, mastering these activities means forming at least 4 new habits. For others it may require dozens of new habits to master them. But forming habits requires willpower, and we only have so much of that. The result is that many people trying to implement the system as a whole feel overwhelmed by the change, and stop.

\r\n

Zen To Done is a short ebook (there's also a paperback) that describes ten habits you can adopt to become fully productive. If even ten habits sounds daunting and unachievable to you, don't worry, Babauta has you covered. He describes a minimalist system that will yield major improvements in productivity consisting of just four habits: Collect, Process, Plan, and Do.

\r\n

Babauta's approach to productivity is the same as his approach to self-improvement. Break down the desired change into a set of behaviors or habits, and tackle each habit one at a time before moving on to the next. He has some quick tips and tricks in the book to help you form these habits, but if you want to go deeper, you should probably read his other book, The Power of Less, or page through the great free content on his blog Zen Habits.

\r\n

You should buy and read Zen To Done. It's cheap, it's an easy read, and it may help you to make the changes you want in your life. But if you don't, here's some friendly advice inspired by Babauta.

\r\n

Give yourself permission to move slowly. Focus on just one habit, until that one habit is mastered. This means there are several other habits in your queue that are not \"done\" yet. You have to be okay with that. You have to accept that first things come first, and trust that those other things will get done. But for now, you must focus on the one thing you have chosen. Remember, you aren't watering the fields quite yet. You are still digging ditches.

", + "date_published": "2013-01-18T11:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 3, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.article", + "pk": 16, + "fields": { + "title": "Take heed, managers: your 'best practices' are killing your company", + "slug": "take-heed-managers-your-best-practices-are-killing", + "status": "usable", + "site": 1, + "description": "To move the needle on organizational productivity, you need to focus on the process by which your company produces value, and constantly improve that process.", + "share_image": null, + "base_template": "", + "content_template": "", + "body": "

If you are a manager, you need to understand the ideas of W. Edwards Deming. Deming wrote several books about management, in which he chastised American business schools and American corporate management for perpetuating a failed philosophy and failed management techniques.

\r\n

Deming proposed a new philosophy of management motivated by quality and grounded in systems theory. The Deming philosophy is too deep, too broad, and too rich to be explained in a mere blog post. Volumes have been written about it, and as I read those volumes I am sharing my thoughts through this venue (with apologies to Mr. Deming if I misrepresent anything, I am still learning.)

\r\n

Probably the best introduction to Deming and his theories is his Red Bead Experiment. The experiment is detailed in Chapter 7 of his book, The New Economics for Industry, Government, Education. The experiment is extremely educational, and I highly recommend you watch it play out in the video below (you’ll need about an hour).

\r\n

\r\n

Deming's Red Bead Experiment

\r\n

In case you haven’t the time to watch the video version, here is the one paragraph summary of the Red Bead Experiment.

\r\n

The experiment simulates a company, the White Bead Corporation, whose job is to ship white beads to its customers. Several employees are recruited from the audience, including line workers and quality control workers. Workers are presented with a box containing 3,200 small white beads, and 800 red beads of the same size. They are given a tool to extract beads from the box 50 at a time, and strict instructions on how to carry out their task of “making” white beads. They use the tool as instructed, then report each batch to quality control for inspection, where the number of defects (red beads) is recorded. The foreman (the instructor) tries several management techniques to improve the performance of his workers: he puts up motivational posters, sets numerical goals, introduces pay incentives, conducts individual performance reviews, and finally lays off the poorest performers. In the end, the company goes out of business because it cannot meet customer demand for defect-free white beads.

\r\n

Now, observers of this experiment can see that the game is rigged. The workers are destined to fail, and that is precisely the point that Deming is trying to make to the managers.

\r\n

“Apparent performance is actually attributable mostly to the system that the individual works in, not to the individual himself,” Deming wrote. Despite the fact that there were observable differences between the output of individual workers, those differences were entirely the result of common cause, that is, the variation is inherent to the system itself. All the efforts spent trying to improve the individual performance of the workers is wasted, because the flaw is not in their performance, but in the system under which they work.

\r\n

Deming warns, “Instead of setting numerical quotas, management should work on improvement of the process.” As a manager, it is your job to understand the difference between special causes that should be remedied individually, and common causes that can only be eliminated with a change to the system itself. And as a manager, the system is your responsibility, not to be delegated.

\r\n

Deming describes how numerical quotas and incentive are not only useless, but actually counter-productive. He gives several examples where workers may report misleading figures, or make poor business decisions that game the system so that the numbers work out. A grocery manager accountable for inventory pulls cashiers to audit a delivery while paying customers wait in line. He stops stocking certain items that move slowly and might spoil on the shelf, forcing customers to shop elsewhere for those items. “He knows 55 other ways to help to meet his allowance of 1 percent shrinkage, all of which hurt the business. Can anybody blame him for living within his allowance?”

\r\n

Listening to Deming, you have to conclude that those annual performance reviews you conduct as a manager are aimed in the wrong direction. Performance of your employees is not a result of the employee’s competence, but of the manager’s competence to build a system in which they can be productive. If your employees are not performing up to standards, you as a manager need to ask yourself what you are doing wrong. What changes must be made to the process of your business to make these employees productive? Ask them, they can probably tell you several, because they actually want to achieve, and they can see what parts of the system are holding them back.

\r\n

Incentive programs and performance reviews are considered management “best practices” and this is why Deming chastised American managers. These practices simply don’t produce the results managers are looking for. Have you ever had a performance review or bonus program result in an order of magnitude increase in productivity? Never. At best, you’ll squeeze out a few percentage points. At worst, you make your employees feel micro-managed and powerless, removing all desire they may have to improve.

\r\n

To move the needle on organizational productivity, you need to focus on the process by which your company produces value, and constantly improve that process.

", + "date_published": "2013-01-22T11:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 2, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.article", + "pk": 17, + "fields": { + "title": "Martin Luther King Jr. Day", + "slug": "martin-luther-king-jr-day", + "status": "usable", + "site": 1, + "description": "People like Dr. King are the most important people in America, people who serve as a national conscience, who remind us of the ideals we aspire to, and insist that we try harder to live up to them.", + "share_image": 2, + "base_template": "", + "content_template": "commoncontent/blocks/article_featuredimage.html", + "body": "

As I write this, I sit in an apartment in Atlanta’s Old Fourth Ward, just a few blocks from where the Rev. Dr. Martin Luther King Jr. preached to his congregation. I cannot express in words the gratitude I feel toward Dr. King and all the thousands of people who marched with him to demand equal rights for all Americans. Since its birth, America has been a nation that aspired to high ideals of equality, and since its birth, America has struggled and failed to live up to those ideals. People like Dr. King are the most important people in America, people who serve as a national conscience, who remind us of the ideals we aspire to, and insist that we try harder to live up to them. Dr. King made us better as a nation.

\r\n

As the events of 2014 made painfully evident, although segregation and racism are no longer the law of the land, they often remain ingrained in the structure of our society. Too many people of color continue to struggle for fair treatment and fair opportunities. We have a long way to go, and a lot of work to do, before we can say we are living up to our American ideals.

\r\n

But growing up, as I did, in the South during the 1970s, attending elementary school in the recently desegregated school system, I learned what a huge impact a few dedicated people can have on society and culture. The cultural difference between the older generation segregationists and the children who were educated in integrated schools was nothing short of stunning to me. While racial prejudice has not disappeared from our culture, that early experience gives me hope that it really can.

\r\n

The Martin Luther King Jr. Holiday was set aside to “serve as a time for Americans to reflect on the principles of racial equality and nonviolent social change espoused by Martin Luther King, Jr.”

\r\n

More than a mere day of reflection, the King holiday has evolved into a national day of service toward the realization of his great dream. The video below explains the King legacy of service, and how you can honor his memory and your community through service.

\r\n

\r\n

MLK Day Legacy of Service

\r\n

Dr. King was a great orator, and although his written words are powerful, I don’t believe you can truly understand the power of those words unless you have heard them as he spoke them. It was not merely the words, but the passion of his presentation, that motivated everyday people to extraordinary action during the civil rights era. I believe everyone should take the time on Martin Luther King Jr. Day to listen to the man speak.

\r\n

If you have children, you owe it to them to teach them about Dr. King, about the struggle for racial equality, and about the nonviolent methods he used to create such great change. If we are going to make this world a better place for all of us, we need Dr. King’s leadership and ideals to live on in future generations.

\r\n

Here are some places to hear Dr. King speak.

\r\n", + "date_published": "2015-01-19T11:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 3, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.article", + "pk": 18, + "fields": { + "title": "How I Get 152 Miles per Gallon in the City", + "slug": "how-i-get-152-miles-per-gallon-in-the-city", + "status": "usable", + "site": 1, + "description": "Density and public transit make urban life far more sustainable than the suburban alternative. Save the planet, move into the city!", + "share_image": null, + "base_template": "", + "content_template": "", + "body": "

How good is the mileage on your car? I just did the math, and I travelled 2,288 miles in the past 4 months on 15 gallons of gas, so I’m getting 152 mpg. And I don’t even drive a hybrid!

\r\n

How is that even possible? Here’s how I did it.

\r\n

My last fuel purchase, according to my financial records, was on October 17th 2014. Today is February 7th 2015. I put 15 gallons of gas in my car today. Every time I fuel up, I set my trip counter so I can calculate my mileage at the next fuel up. I have driven 245 miles since my last fuel purchase 113 days ago.

\r\n

“Wait,” you’re probably saying, \"you haven’t bought gas in 113 days? I fill my tank every week! What are you trying to pull here?”

\r\n

It’s simple, friends. You see, I live in the city. My home is walking distance to public transit, and I ride the train to work every day instead of driving. My home is also walking distance to the grocery store, pharmacy, local restaurants, dry cleaners, and almost everything else I need. So I almost never drive my car.

\r\n

According to the app on my (allegedly smart) phone, I have walked an average of 3.4 miles per day over the last 4 months. Multiply by 113 days, and we find that I have walked 384.2 miles since my last fuel up. That’s about 50% more walking than driving.

\r\n

During that 113 days were some holidays, some vacation time, and a few work-from-home days, so I commuted to work and back 58 times. The distance between the two train stations, if I drove it, would be 14.3 miles. So at 28.6 miles per day, I commuted 1658.8 miles getting to work and back by train.

\r\n

That’s 245 miles driving, 384.2 walking, and 1,658.8 by train for a total of 2,288 miles on 15 gallons of gas, or 152.5 miles per gallon.

\r\n

So if you think urban living is bad for the environment, think again. Density and public transit make urban life far more sustainable than the suburban alternative. Save the planet, move into the city!

", + "date_published": "2015-02-09T11:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 3, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.article", + "pk": 19, + "fields": { + "title": "A New Mission for Media", + "slug": "a-new-mission-for-media", + "status": "usable", + "site": 1, + "description": "Focus on the essential mission of the media, to facilitate the flow of information to the point of its highest value, and never stop asking how you can better serve your audience. Do this, and you will turn your foundering media business into a rapidly growing information services company.", + "share_image": null, + "base_template": "", + "content_template": "", + "body": "
\r\n

Facilitate the flow of information to the point of its highest value.

\r\n
\r\n

The media industry at large has lost its path. Most media companies are heavily tilted toward media as entertainment, rather than media as information. As a result, they are engaged in a digital race to the bottom, where falling ad CPM drives them to seek higher page view numbers on thinner margins, focusing on quantity rather than quality, on usage rather than utility. This has left a huge, blue ocean of market opportunities in focused information services open to software and technology companies, who are growing at exponential rates while traditional media companies struggle to slow the rate at which their business is shrinking. Media businesses can stop drowning and start growing again if they recognize and adopt the mission statement above, the mission that media organizations have always had.

\r\n

As a media organization, your mission is to facilitate the flow of information to the point of its highest value.

\r\n

That doesn’t just mean its highest value to you, the business, but the highest value over all to the community you serve. Journalism can be seen as a fulfillment of this mission, because information about corporate malfeasance or government activity has more value to society when it is in the hands of consumers or voters. But media organizations, and especially newspaper organizations, need to embrace the idea that news is more than just journalism, and media is more than just news.

\r\n

A perfect example of an information service that should have been, but was not, created by a media company, is Waze. Waze is a mobile application that feeds its users real-time traffic information and turn-by-turn directions. Now, media companies have been providing their users with traffic information for decades, even going to the expense of paying for aerial observation helicopters to monitor traffic conditions. They knew there was a market need there. They thought they were filling it. Then Waze came along and stole their thunder, because Waze gives users exactly the information they need, at the exact time that they need it, and that makes the information much more valuable to the consumer. The fact that you publish the same information on your web site or broadcast it over the radio is irrelevant. Waze provides that information at the point of its highest value. Waze is doing the job of a media company.

\r\n

Why didn’t a media company invent Waze? In fact, why didn’t every local media company across America re-invent it? It’s certainly not because they didn’t know about the market need. I think it’s because they lost sight of their mission. They were thinking about attracting audiences to their products, when they should have been thinking about how to serve their audiences better. Had they been focused on facilitating the flow of information (rather than merely the flow of advertising dollars), they might have recognized the opportunity.

\r\n

The proliferation of personal, always-present, always-connected, location-aware mobile devices presents a huge growth opportunity for media businesses. More than ever before, it is now possible to deliver information to your audience at the exact time and place that it will have the most value.

\r\n

You, as a media business, should be brainstorming every day to find new situations where a specific person needs some specific information at some specific time, and crafting new products for those situations. Some of these products will be so valuable that your audience will pay for them. Some will be better suited to an ad-supported model. Still others may be sponsored by a single business or organization. Some may lend themselves to in-app purchase opportunities. By addressing the timely information needs of your audience, you open the possibility of multiple revenue streams. If you perform this brainstorming exercise, you will come up with many, possibly hundreds of ideas.

\r\n

It is your responsibility, media executives, to structure your business as a pipeline for testing and scaling these ideas. Train your audience development team in the latest (fastest and cheapest) user research methods. Train your product development team on the latest mobile and web technologies. Leverage the on-demand compute power of the cloud to reduce up-front capital outlays, quickly shut down experiments that don’t pan out, and rapidly scale the ones that take off. Use application templates to shrink your time to first prototype. All of these are ways to reduce the cost and risk of testing new products.

\r\n

Get your assembly line running and don’t let it stop. Ship a new app every quarter, then every month, then every two weeks. If they don’t make a splash, no worries. It’s a small investment, easy to write off. If they do hit, scale them up and leverage the new revenue streams.

\r\n

The key is to focus on the essential mission of the media, to facilitate the flow of information to the point of its highest value, and never stop asking how you can better serve your audience. Do this, and you will turn your foundering media business into a rapidly growing information services company.

", + "date_published": "2015-04-15T10:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 1, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.article", + "pk": 20, + "fields": { + "title": "The Tale of the Tail", + "slug": "the-tale-of-the-tail", + "status": "usable", + "site": 1, + "description": "For nearly twenty years, the most concise description of me was \"the guy with the really long pony tail.” This is the story of how it came to be, and why it is no more.", + "share_image": null, + "base_template": "", + "content_template": "", + "body": "

For nearly twenty years, the most concise description of me was \"the guy with the really long pony tail.” That’s no longer the case, and I am frequently asked why. I find it curious that there is such a widespread assumption that a person needs a reason to change their hairstyle, but as it happens, I do have one. This is the story of how it came to be, and why it is no more.

\r\n

From 1989 to 1995 I was an active duty Marine, which meant that I wore my hair very short, and had it cut every single Friday. When I left the military, I found myself needing to make a decision about hair style for the first time in years. At the time, I had been reading a lot of Viking Sagas, and I was reminded of King Harald Finehair, who first united all the kingdoms of Norway. He received his epithet because he had sworn an oath not to cut his hair until he reigned over all of Norway, and he stuck by the oath for the ten long years it took him to achieve his goal. Being young and still somewhat foolish, I decided to make a similar romantic gesture. I would swear an oath not to cut my hair until….

\r\n

In order to understand that oath, it might be helpful to understand my circumstances at the time. I was 25 years old, freshly discharged from the military after six years, two overseas deployments, and one war. My family was working class, with no assets to speak of, living from paycheck to paycheck, so there was really nothing for me to fall back on. The most important relationship of my life had just broken up. I was drifting, unsure where to go, or what job I should be looking for, or really just how to live in the world. I was working a dead-end job for well below the median income, and wondering if that was all I was worth. That high school I-can-do-anything confidence had now faded into the I-have-to-pay-the-rent real world, and the impedance mismatch was tearing at my soul. In short, I was miserable, lonely, lost, and broke.

\r\n

And so I swore an oath. I would not cut my hair until I was satisfied with my life. And I would tell no one what the goal actually was until it was achieved. Because talking about it was my litmus test. I didn’t feel comfortable talking about how uncomfortable I was in my own skin, and in my own life. I knew as long as I felt that way, I would not have achieved my goal.

\r\n

At the time I swore that oath, I had a very naive vision of what it meant to be satisfied with one’s life. I imagined myself independently wealthy, with washboard abs, thrilling women with my fabulous athletic body and intimidating men with my real estate portfolio and unshakeable self-confidence. I proceeded to work toward that vision with great vigor. And I became even more depressed.

\r\n

I settled in to daily life, merely trying to get by, that oath and the hair it spawned always right behind me, at the back of my head. Slowly, over the years, I found opportunities to grasp the things I had told myself were the brass ring. I found a career that I loved. My income steadily increased. I bought property. I dated women far more attractive than I deserved. I fell in and then out of love. Every time one of these milestones passed, I would ask myself, is this it? Is this the magic line where I become satisfied with my life? Every time I found that, having crossed the goal line, I felt no different about my life than I had before. Eventually, I just resigned myself to the idea that I would never get there, ever. And I stopped talking about my oath.

\r\n

Then came the crash. I lost my job. I lost my girlfriend. My favorite pet died tragically young. The world came crashing down around me, and I lost all hope. The despair of being unable to achieve, and of being unable to appreciate anything I did achieve, led to an emotional crisis. I even contemplated suicide. I broke in a spiritual and psychological sense.

\r\n

And when I did, my eyes opened. I saw, somehow for the first time, that I was not alone. I had friends around me who actually cared. Not many (because frankly I had been an insufferable jerk in my self-involved despair), but enough to have someone to talk to, someone to lean on. With the help of these friends I learned to see life from other perspectives. I came to understand what my life looked like from their point of view, and how they looked at their own lives. I discovered that everyone was just as lost as I was.

\r\n

One friend took me to church with her, and though I was not a Christian, I found solace and friendship there. Religion was not ultimately my path, but I gained immense insight by learning to see the world from their perspective. I learned to understand finally the concept of Faith, and the way God helps everyday people, not through miracles, but by helping to lift their emotional burdens.

\r\n

I began to study the Buddha Dharma, because it promised relief from dukkha, which is often translated as “suffering” but really encompasses a fundamental dissatisfaction in all things, and I felt this was an apt description of my inner state. I learned that the inner core of soul one identifies as “self” is an illusion we create, and that is why, no matter how we try, we can never reconcile it with the reality of our lives. I learned that all things are impermanent, and that our illusory “self” becomes attached to impermanent things. When those things change, as they must, the self suffers from the attachment. That suffering is dukkha and it is a condition of life.

\r\n

I also learned about Imposter Syndrome, and thereby discovered how very common it is for someone to achieve “success” yet be unable to feel successful. More and more I learned that my own experience was merely The Human Experience.

\r\n

To turn a cheesy movie quote into deep philosophy, there is a difference between knowing the path, and walking the path. Although I was learning these new concepts and using them to understand my past failures, it was difficult to understand how to apply them to my daily life. I continued going through the motions of life, trying to understand my failure, trying, as I phrased it, to learn to live in this world as a human being.

\r\n

I took the advice of the Dalai Lama and began to practice compassion intentionally. Everyone in the world was suffering the way I was, and if I could not cure my own suffering, perhaps I could help alleviate theirs. The fact that I often found it surprisingly difficult to feel compassion for certain others convinced me that this was the right path. I still struggle with it. I’m sure I always will.

\r\n

Most of you who know me today never met that early Vince. Only a handful of friends managed to hang with me through that time (and thank you all, I love you). Those who met me afterward might not even recognize that angry, depressed, and confused young man. The crisis and transition I describe above is now fifteen years in my past.

\r\n

This is what led, fifteen years later, to the resolution of my oath, and the cutting of my hair. The intentional practice of compassion allowed me to internalize that life is not about things, it is about people, and compassion is what holds people together. I steadily worked to replace attachments with relationships, impermanent though they may be. As I aged into mid-life crisis territory, I increasingly appreciated the people in my life. And I began to examine the things in my life with great scrutiny. Do I really need this thing? Does it provide me any value, any satisfaction, any joy? Thing after thing went on the rubbish heap as the answers kept coming back negative.

\r\n

And so finally, this year (2015), I found myself asking of my oath, and the hair that represented it, do I really need this thing? Does it provide me any value? Any satisfaction? Any joy? I was frankly astonished to find myself answering in the negative. I don’t need it anymore. I am comfortable with myself, satisfied with my life, and in fact I have been for a few years now. I don’t need that hair anymore. I don’t need that oath anymore. I am, finally, comfortable with being me, and comfortable talking about being me.

\r\n

Don’t get me wrong. I still have goals, and too many failings to count. There are many things in my life and in my self that I want to change, and I will keep working at them. It is a very long journey, and I am only in the middle. But I have achieved that young man’s goal, in a most unexpected way. Neither wealth nor status nor outward appearance brings satisfaction with one’s life. Through the practice of compassion for others, I have discovered that being satisfied with one’s life means having compassion for oneself.

", + "date_published": "2015-07-04T10:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 3, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.article", + "pk": 21, + "fields": { + "title": "The Worst Book I Will Ever Write", + "slug": "the-worst-book-i-will-ever-write", + "status": "usable", + "site": 1, + "description": "Something I have been struggling with during the work on my (current) first novel is this: I am very likely producing the worst book I will ever write.", + "share_image": null, + "base_template": "", + "content_template": "", + "body": "

Something I have been struggling with during the work on my (current) first novel is this: I am very likely producing the worst book I will ever write.

\r\n

Assuming I finish it — which given past history is not a foregone conclusion — suppose I find the whole thing too exhausting to do again? Having written only one, it would, by default, be the worst one (and the best I suppose). If I go on to write the second, I should have learned much from writing the first, and the writing should improve. Skills are supposed to improve with practice, are they not? So I am destined to look back on this book and recognize how bad it is.

\r\n

But there’s no getting around that. It was the same thing when I was learning to write computer software. Every six months, I would look back on my code from six months prior, and be disgusted by how bad it was. Most programmers recognize this feeling. It’s how we know we’re getting better. If ever we look back on old code and feel satisfied with it, it’s a sign that we’ve stagnated, and maybe it’s time to look for a different career. Have you considered middle management?

\r\n

I expect I will find writing the same. I will struggle to do my best with each succeeding story. And every time I look back, I will be sickened at how badly I botched it. But I will be determined to take my lessons learned to make the next one better. And if ever I look back on a story I’ve written and feel fully satisfied with it? That will be the sign that I’m done writing, I guess.

", + "date_published": "2016-11-05T10:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 4, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.article", + "pk": 22, + "fields": { + "title": "The Thing About Life Is...", + "slug": "the-thing-about-life-is", + "status": "usable", + "site": 1, + "description": "Why \"failure\" is a lie, and should never stop you.", + "share_image": null, + "base_template": "", + "content_template": "", + "body": "

Most people like to post on the Internet about their successes. I want to talk about failure. I can assure you, I am fully qualified to talk about failure. I’m an expert with thirty years of experience.

\r\n

You see, the past few months I have been thinking pretty hard about where I am in my life, especially in relation to the dreams and aspirations I had as a teenager. I’m forty six years old now. Thirty years ago, I was sixteen, and I had big ambitions. I wanted to publish books. I wanted to make money in real estate. I wanted to find love.

\r\n

So thirty years on, how am I doing? Two failed marriages, with a handful of failed relationships that never got that far. Finding love? FAIL. Two real estate purchases, one that ended in foreclosure, the other is worth less now than when I bought it, yet I owe more than I paid for it. Making money in real estate? FAIL. Number of books published: zero. Number of books written to completion: zero. Number of failed attempts: [I’ve lost count.] So, publishing books? FAIL.

\r\n

You might be thinking, dude (or some equivalent form of casual address), you’ve been failing to achieve your childhood dreams for thirty years? Don’t you find that, you know, kind of depressing?

\r\n

Frankly? Yes. Sometimes I do. Sometimes it crushes me under the weight of my failure so hard that I lie in bed and cry myself to sleep. But, just as frankly, that’s a fairly rare occurrence. You know what I do most days?

\r\n

I keep trying.

\r\n

You ever play Hacky Sack? It’s this game, or activity really, with this little bean bag, a little bigger than a golf ball. You’re supposed to kick the bag up in the air. When it comes down, you try to kick it again to keep it in the air. That’s basically it.

\r\n

I figured out, a long time ago, that life is a game of hacky sack.

\r\n

First off, playing it alone is really hard, and not much fun. Two people can do alright, but the game doesn’t get really fun until you have four or five people in a circle playing together.

\r\n

The object is to keep the sack in the air, but you begin the game knowing that you are doomed to failure. You know, with compete certainty, that the little bag is going to hit the ground. When it does? You pick it up and start again.

\r\n

“Success,” if there is any in the game, comes when every player in the circle gets in a kick before the sack hits the ground. But, there isn’t any score keeping. There’s no competition. There’s no way to win.

\r\n

The object of the game is just to keep kicking.

\r\n

So I still hit that keyboard every day, adding words (currently 47,547 of a target 80,000). Some days, all I can do is stare at the screen. Some days, I’m so swamped I can’t even look at the computer. Tomorrow, I will try to add 1,000 words.

\r\n

I’m still hammering away at the house, in the literal sense as well as the figurative, working to turn it into something that people would pay money to live in. Most days it feels like the building is out to get me. More than once it has injured me physically. This weekend I’ll be repairing the overhead lights in the den. Again.

\r\n

Love? I still believe in it. I still have hope of having a love all my own one day. Until then, I have a couple of very close friends, you know, not the type who will help you move, but the type who will help you move a dead body. And I have far more friends of the help you move type, with whom I share mutual respect and the occasional beverage, than I ever expected to. Tomorrow, I’m having drinks with a bunch of them. And who knows what happens next?

\r\n

Maybe I never actually make money on that house. Maybe that book never quite makes it (but I think it will). Maybe that perfect someone will simply never be mine.

\r\n

But I’m still here. And I’m just going to keep kicking. Because that is the point of life.

", + "date_published": "2017-01-23T11:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 3, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.article", + "pk": 23, + "fields": { + "title": "Creating a Creative Process", + "slug": "creating-a-creative-process", + "status": "usable", + "site": 1, + "description": "All writers have a process (I’ve heard). Some writers are \"discovery\" writers or \"pantsers,\" who begin with a character or situation and discovery the story as they write it, \"by the seat of the pants\". I am not one of these writers.", + "share_image": null, + "base_template": "", + "content_template": "", + "body": "

I always aspired to being an author. In my teens and early twenties, I probably started a half-dozen \"first novels\" that never got finished (and that was probably for the best). When I started writing software, though, that became my creative outlet, and I stopped writing fiction. I even stopped reading fiction for many years.

\r\n

In 2015, I decided to pick up that dream once again, and I determined to set myself to finishing a first novel. I dusted off some old ideas, tossed in some new twists, and began putting words down.

\r\n

And I quickly realized that it wasn’t going to work.

\r\n

All writers have a process (I’ve heard). Some writers are \"discovery\" writers or \"pantsers,\" who begin with a character or situation and discovery the story as they write it, \"by the seat of the pants\".

\r\n

I am not one of these writers. What I learned after ten thousand words or so, was that I needed a structured process. I am an outliner. And my outline was not strong enough to hang the writing on. I got stuck.

\r\n

So I hung up the dream of finishing my novel during 2015’s NaNoWriMo, and resolved to take a more disciplined approach.

\r\n

Currently I’m using a process similar to the one I use to create software. I’ve laid down the essential elements as cards in Scrivener: the main themes of the story, key scenes, major events. I arranged the events chronologically, and began to break down the story into acts, chapters, and beats. I’ve heard television writers talk about \"breaking\" a story in exactly this way to create an episode. That must be a much harder task for television writers, where the acts need to be strictly timed and fairly uniform in size. Fortunately (or unfortunately?) a novelist has a bit more freedom.

\r\n

As I began to visualize my story outline this way, I discovered major problems. First, a huge amount of the action was bunched up at the beginning. The entire middle was empty of any significant action. And there was no bridge from that empty middle to the resolution.

\r\n

This is where Scrivener helped me a lot as a tool. Since all my scenes were tucked into these nice index cards, I started to shuffle them. I broke up that tight cluster of cards at the beginning and started to distribute them into the middle chapters. Then, realizing that there was still a huge empty space where the second act was supposed to be, I knew I needed to add a major subplot there to carry it forward.

\r\n

With a strong outline, the process of actually laying down the words becomes much cleaner for me. I know more or less exactly what it is I need to say, and I can focus on the craft of how to say it. These are two distinct tasks — determining the action, and describing the action — and for me they require two different mindsets. Creating a detailed outline helps me separate them.

\r\n

This led me to the (re)discovery that building the story and telling the story are two very different things. As a writer, I need to understand the chronology of the entire arc, and the back story, and (in my case) the future story as well. Since I am writing fantasy and building a world, I have history and cultures to flesh out also.

\r\n

But not all these facts will be revealed directly to the reader. Some things that chronologically happen at the beginning won’t be revealed to the reader until the end (that’s the definition of a mystery, isn’t it?). Some of it is backstory or background color that will be sprinkled into the middle, as characters talk about their past (off-screen) interactions.

\r\n

I’m finding that, if I’m going to do anything interesting in the telling of the story (mysteries, flashbacks, parallel timelines) I actually need two outlines: one a timeline of events that have happened, for me to keep things straight, and the other an outline of what gets revealed to the reader, the structure of the story. I haven’t really figured out how to do that. Scrivener is great at the story structure part, but it feels clunky using it for the author timeline. I know there are some dedicated timelining tools out there. Maybe I should look into them.

\r\n

There’s plenty more I haven’t figured out. I know I need to mix character beats with plot beats with world building info-dumps. Currently I’m still \"pantsing\" that, and I don’t feel like it’s very effective. I would like to be more deliberate about exposing the world building and character stuff in the outline, instead of painting it haphazardly onto the plot as I write. On the other hand, a lot of those things don’t come up until I’m in the character’s head in the moment, and I’m like, wait, she’s writing a letter, but was there even a post office in the middle ages? How DID they deliver the mail? And is she sad, or relieved, that she doesn’t have a husband during this trial?

\r\n

It took me many years to learn how to build software well. Doubtless, it will take years for me to reach journeyman level in the fiction craft as well.

", + "date_published": "2017-02-19T11:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 4, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.article", + "pk": 24, + "fields": { + "title": "NaNoWriMo 2017 After Action Report", + "slug": "nanowrimo-2017-after-action-report", + "status": "usable", + "site": 1, + "description": "I wanted to record for posterity my first experience of \"winning\" National Novel Writing Month.", + "share_image": 3, + "base_template": "", + "content_template": "", + "body": "

I wanted to record for posterity my first experience of \"winning\" National Novel Writing Month. As you may know, NaNoWriMo is a self-challenge to write 50,000 words in 30 days.

\r\n

Throughout 2017, I have been tracking my writing statistics (screenshot provided). As you may see from the screenshot, before November, I never got anywhere near 50k words in a single month. So I took NaNoWriMo 2017 as a true challenge to get my word counts up and make some real progress, to prove to myself that I could truly improve both speed and quality.

\r\n

\"Nano

\r\n

Background: The Story So Far

\r\n

The novel I worked on in November 2017 was not my first. I started a novel back in November 2014, but did not win NaNo, or even come close. What I learned from that experience is that drafting a novel (what most people call \"writing\") takes preparation. I needed to know my characters, and my plot, better, or else I would spend all my writing time noodling around, doing \"research\" on the Internet (read \"following Wikipedia links for hours\"), and doing almost anything except actually producing publishable words on the page.

\r\n

After struggling with that book for two years, I decided I needed a break. Not a break from writing, but a break from that particular book. The style and subject I had chosen for that book made it a very time-consuming task, and I wasn’t accelerating. I knew I needed to accelerate if I was ever going to reach my final production goal of producing four books a year.

\r\n

So in 2017, I changed tacks. I also changed projects, and characters, and genres, and writing styles. Most importantly, I changed my focus. I put aside all my other optional projects so I could focus on writing.

\r\n

If you look at my word count charts, they illustrate the story. In January, I wrote a scene. I had just read Farewell My Lovely by Raymond Chandler (if you listen to audio books, this one should not be missed), and I wanted to experiment with writing in that style. First person, but without internal monologues. Filled with over-the-top, sometimes hilarious metaphors and similes. Varied sentence length that leaned heavily to the short side. Characters that were hard, and dark, and mysterious.

\r\n

So, I wrote a scene, off the top of my head, focusing on the style and letting the content be just whatever popped into my head. Didn’t matter if it made sense.

\r\n

Long story -> short, I liked the output, and wanted to do more. But I now had this scene, and these characters, and all these little hints in the scene about secrets, and I didn’t know where any of it went! After that scene, I understand the thrill that \"pantsers\" have when they are writing. (And after working on the full project, I understand why I will never be a pantser.)

\r\n

That gap in drafting in February is partially time I spent trying to flesh out the world, the characters, and the story that was born in that first scene. (And partially that I had no time to write because I just moved into a house while it was still under remodel, and it was cold, and the heat wasn’t working yet. Sigh.)

\r\n

In March, I had produced an outline, I had a draft target of 55,000 words, and I was ready to go. I also decided to be meticulous about tracking my writing progress, because I’m a big nerd. My naive goal was to produce an average of 1,000 publishable words each day. Still not there. But getting better.

\r\n

Through March, April, and May, I worked on my first draft. As you can see from the chart, April was the stand-out month for me. I had 16 writing days, and my per-day average was far higher than before. That’s because I created (what I thought was) a solid outline of the book before I started drafting. I knew where the story was going, so each day when I sat down, I knew exactly what I needed to work on. And, check it: I finished the book! Woohoo!

\r\n

Lessons learned from the first completed book:

\r\n
    \r\n
  1. The draft came in about 20% shorter than I had planned. Apparently I am a \"short writer\". Revisions need to add words more than remove them. That’s cool. Good to know.
  2. \r\n
  3. There was a point in the middle where the outline fell apart. I had to stop drafting and start outlining again. I would need more-detailed outlines in future.
  4. \r\n
\r\n

I spent the rest of May and most of June revising and adding to the book. That’s why the word counts per day are much lower there. I was doing as much reading as writing, and I was padding out and enhancing scenes rather than writing them from whole cloth.

\r\n

In June, I spent a couple of weeks outlining a sequel. In July I commenced drafting the sequel. July and August were good months for drafting. At the end of July, I looked back at my stats. I found that my words per day were pretty solid, but I wasn’t hitting my desired average over time, because I was not getting enough drafting days in. So I decided that, in August, I would focus on writing every single day, and not care so much about word count. I did really well on that for the first thirteen days of the month.

\r\n

And then my outline blew up again. So I stopped writing, and went back to the drawing board to figure out what to write.

\r\n

The Run-up to NaNoWriMo

\r\n

By the time I had that outline fixed, I realized that I only had a couple of months to go before NaNo. I was determined to win NaNo this year with what I learned. I knew to win, I needed to have a really thorough outline. I knew my timing would be off if I continued with the current project, so I shelved it. (Sorry Tracy! I will finish it, I promise!)

\r\n

I started researching for my epic fantasy series. (How do you research fantasy? More about that in a future post.) Then, I started building an outline. I built it first using the same basic process as before, just make a card for each event. But I had learned that an event is not necessarily a scene or a chapter. Sometimes, it’s just a page, or a sentence. So then I went back, and started stacking those events. New Stuff:

\r\n

I made a list of POV characters and assigned scenes to them.

\r\n

I reshuffled the outline to get thematic alignment between the story threads.

\r\n

I ran it through the Story Grid and Save the Cat frameworks to make sure I was hitting the right beats. I even looked at it through the lens of Dramatica (more on that in another post too). I added scenes and characters to bring it into alignment.

\r\n

Once I had beaten the book outline into submission, I went back to the beginning and started outlining each individual scene, making sure it had an inciting incident, a turning point, an emotional charge, and as much as possible, a dramatic conflict.

\r\n

Three days before November began, I could see that I was not going to finish my individual scene outlines in time. I had 42 of them to do, and each one was taking about an hour. I decided I would create them as I went along.

\r\n

Since my outline called for an 82,000 word draft, I decided to go ahead and start drafting.

\r\n

Thirty Days of NaNoWriMo

\r\n

The first few days, I was drafting scenes for which I had created scene outlines. This was so easy it felt like cheating. I already knew the emotional content, the character conflicts, the implications for future chapters. I blasted through my target word counts each day. The first day I missed target was the 7th, but that was because somebody broke into my house and tossed the place while I was at work, and I had to spend my writing time dealing with that. (Real life sucks.)

\r\n

Couple of days later, my scene outlines ran out, and I had only single-paragraph descriptions to work from for each scene. Some of those scenes were already well-formed in my mind, and those days produced more than 2,000 words. Other scenes still needed the outline work put in, and on those days, I struggled. Even on the slow days, though, I was usually able to break at least 1,000 words.

\r\n

For most of the month, I was well ahead of the target curve. But, life happens. I had a couple of days when I was too busy to write, and a couple that produced very few words, because of reasons. I was falling behind. But I was so close!

\r\n

To make sure I caught up, I decided to switch from my linear approach (writing scenes in the order they would appear in the book) to a non-linear selection approach (writing scenes that had the strongest outlines, and connected more directly to scenes already written). In that way, I was able to power through that last weekend and get back on top. It took me until the final day of the month, but I did, finally, cross the 50k mark. I win!

\r\n

November 2017 was my most productive month ever! I drafted on 28 of 30 days, and yet my average per day was higher than ever before! I attribute this to the intense work I put into outlining the book, and then the individual scenes, before the drafting phase began. For my next book, I will complete the scene-level outlines before I begin drafting.

\r\n

As I write this, I still have about 25k words to write in the first draft of this book. And I can tell you, that first draft is rough. It’s going to need a lot of work. But it’s also clear to me that I am improving as I go. If I keep working, keep learning, keep accelerating, I will soon hit that thousand word a day goal. And may even produce a book worth publishing!

", + "date_published": "2017-12-11T11:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 4, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.article", + "pk": 25, + "fields": { + "title": "Cursing Fate Cover Art! And a FREE offer!", + "slug": "cursing-fate-cover-art-and-a-free-offer", + "status": "usable", + "site": 1, + "description": "", + "share_image": 4, + "base_template": "", + "content_template": "commoncontent/blocks/article_featuredimage.html", + "body": "

Cursing Fate: The Piero Codex Book One, is now available for pre-order on most major ebook retailers! This is the first book in my urban fantasy thriller series, The Piero Codex. Books two and three will follow quickly in a rapid release schedule, so don't worry, you won't have to wait long to read the whole trilogy.

\r\n

I am unspeakably excited that my first book is finally available to the world. In fact, I'm so excited that you can call me Giveaway Bob, because I'm giving them away!

\r\n

For a limited time, just for being an early fan, you can get the

\r\n

Cursing Fate ebook for FREE! Click here to get it!

\r\n

And if you need convincing, check out this gorgeous cover (by Barb Hoeter at Coverinked)!

", + "date_published": "2018-03-28T10:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 5, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.article", + "pk": 26, + "fields": { + "title": "Shifting Loyalties: Cover Art and Pre-order", + "slug": "shifting-loyalties-cover-art-and-pre-order", + "status": "usable", + "site": 1, + "description": "", + "share_image": 5, + "base_template": "", + "content_template": "commoncontent/blocks/article_featuredimage.html", + "body": "

Big announcement! Shifting Loyalties: The Piero Codex Book Two is now available for pre-order on most major ebook retailers!

\r\n

Barb from CoverInked really stepped up her game on this cover, don't you think?

\r\n

When it’s a matter of life and death, how do you know who to trust?

\r\n

Mack was never good at trusting people, even before he went underground hiding from the Seers Guild. But when the former love of Mack’s life (Marina) goes missing, and an unknown organization makes dramatic moves against the Protectors of the Piero Codex, he needs help.

\r\n

Maybe he can trust Recca, but she still seems to have a hidden agenda of her own. Maybe he can trust Lilly, but will involving her just get her killed? Maybe he can trust Marina’s husband Richard, but how is he supposed to work with the guy who stole his girlfriend?

\r\n

With the enemy closing in, and time running out, turning an enemy agent seems like the best option. But when you can’t even trust your friends, how do you trust an enemy? Even if he manages to make all the right choices, how is Mack going to convince the other players to trust him long enough to save Marina and protect the Codex?

\r\n

This second installment in The Piero Codex series takes you deeper into the world of seers, and reveals some of the secrets of the Codex itself! Get it today! (Pre-orders will be delivered June 14, 2018.)

\r\n

Where To Get It

\r\n\r\n

 

", + "date_published": "2018-05-09T10:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 5, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.article", + "pk": 27, + "fields": { + "title": "Dramatica: My Take-Aways", + "slug": "dramatica-my-take-aways", + "status": "usable", + "site": 1, + "description": "There is some genius in this theory, and possibly some madness. Every writer should at least understand Dramatica's four through-lines, its definition of character archetypes, and the two helpful concepts of character resolve and story limit. If you go deeper, prepare to be confused.", + "share_image": null, + "base_template": "", + "content_template": "", + "body": "

TL;DR: There is some genius in this theory, and possibly some madness. Every writer should at least understand Dramatica's four through-lines, its definition of character archetypes, and the two helpful concepts of character resolve and story limit. If you go deeper, prepare to be confused.

\r\n

The high-level concept in the Dramatica theory is that a story is a model of a human mind attempting to solve a problem. A \"complete story\" is a story that examines all possible solutions to a single problem.

\r\n

Dramatica comes with a bundle of self-imposed caveats. It admits that it is a theory that describes a certain kind of story. It also admits that a good story structure doesn't necessarily lead to a good story, and there are plenty of successful stories in the world with incomplete structures (according to the Dramatica theory). Like several other popular story structure theories, it was developed around Hollywood movies, and doesn't adapt perfectly to other storytelling forms.

\r\n

This theory is extremely complex. It invents a vocabulary all its own for analyzing stories, which makes it difficult to absorb quickly. I'll be honest and say up front, I don't have a deep understanding of it. If you want to go deeper, see the resources at the end.

\r\n

That said, this theory has some insights that I have found extremely valuable in constructing and understanding stories. Here they are.

\r\n

Dramatica's Four Through-Lines

\r\n

Dramatica identifies four through-lines that intertwine to weave a complete story.

\r\n

The Overall Story Through-line: This is the \"big picture\" story, the broad scope, or what I sometimes think of as the plot.

\r\n

The Main Character Through-line: The story as seen through the eyes of someone in it. The \"main character\" is the primary point-of-view character, the person we expect the audience to most empathize or identify with.

\r\n

The Influence Character Through-line: Also called the Impact Character, the IC's perspective on the story is different from the main character's. The IC attempts to influence the MC to change their perspective or approach.

\r\n

The Subjective Story Through-line: Also called the Relationship, this is the relationship between the main and influence characters in the context of the story.

\r\n

Explicitly identifying these four through-lines makes it easy to see why some stories seem to have no \"heart.\" Those plot-driven stories focus on Overall Story and give little attention to Character and Relationship.

\r\n

The interplay of plot, character, and relationship is what makes stories really stick. I like to say: Plot is what happens. Story is how your characters change (or don't) because of what happens.

\r\n

Character Resolve

\r\n

Speaking of change, another extremely useful concept I picked up from Dramatica is character resolve. In Dramatica the theory focuses on the Main Character Resolve, assigning it either Change (the main character changes their perspective or approach by the end) or Steadfast (the main character retains their original perspective or approach at the end).

\r\n

This part of the theory addresses what is normally called the character arc. Arcs come in pairs, according to the theory. The Main and Influence character have different approaches to the story problem. At the end of the story, one of them must be proven right, and remain Steadfast, the other must be proven wrong and Change.

\r\n

There are a lot of story theorists who think that the main character must change, even going so far as to define the main character as the one who changes. Those theories never sat well with me. Dramatica's insight that a character is tempted to change, but may remain steadfast, encompasses a much more diverse set of great stories and dramatic possibilities.

\r\n

While Dramatica primarily cares about the main character's resolve, it seems to me that any character who has an arc must have a resolve, and probably needs to have their own influence character to reflect the opposite resolve to make the story feel complete.

\r\n

Character Archetypes: Protagonist vs Main Character

\r\n

Briefly, Dramatica identifies eight character archetypes: Protagonist, Antagonist, Reason, Emotion, Sidekick, Skeptic, Guardian, and Contagonist. The theory goes into great detail explaining how these archetypes are constructed, and how they can be broken down to create more complex and interesting characters. I can't do the theory justice in a short summary. Go check it out for yourself.

\r\n

The key insight that I found most useful here is the clear distinction between the Protagonist (the driver of the Overall Story) and the Main Character (the primary POV character).

\r\n

According to the vocabulary of the theory, the Protagonist is the character who pursues the Overall Story goal. The Main Character is the character the audience most identifies with, the one through whose eyes we see. When these two roles are played by the same character, they are called a Hero. This is a common construction, but not the only one.

\r\n

A more complex story can separate these roles into different characters. The standard example is To Kill A Mockingbird, where the Main Character is young Scout, but the Protagonist is Atticus Finch.

\r\n

Story Limit

\r\n

Finally, Dramatica helped me understand the concept of a story limit. You have seen story limits in action stories where there is a literal ticking clock. If the clock counts down to zero, the hero loses! The ticking clock is called a Time Limit. It's so prevalent that Story Grid creator Shawn Coyne assigned an entire sub-genre to it.

\r\n

Dramatica clued me that there is another kind of story limit: the Option Limit. This is where the story is given finite scope by allowing limited options: three wishes, and then you're done.

\r\n

The story limit is the thing that adds dramatic tension to your story and compels the characters to move forward. Without a story limit, stories can just meander on and on, reaching no conclusion, or characters can sit around indefinitely without moving the story forward at all.

\r\n

Final Thoughts

\r\n

Dramatica also has some interesting things to say about act structure, showing how two, three, and four act structures can be derived from the value shifts among the four through-lines and how they are distributed through the story. There's much, much more to Dramatica; too much for me to wrap my head around.

\r\n

I have found that story limit, character resolve, and the four through-lines have been excellent tools for me to apply in the construction of stories.

\r\n

To learn more about Dramatica:

\r\n", + "date_published": "2018-06-08T10:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 4, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.article", + "pk": 28, + "fields": { + "title": "Shifting Loyalties Is Available Today!", + "slug": "shifting-loyalties-is-available-today", + "status": "usable", + "site": 1, + "description": "", + "share_image": 5, + "base_template": "", + "content_template": "commoncontent/blocks/article_featuredimage.html", + "body": "

You can stop holding your breath! Shifting Loyalties: The Piero Codex Book Two has been released! (Okay, I know you weren't really holding your breath, but I was!) This is my second novel, as well as being the second book in The Piero Codex series.

\r\n

I'm excited to introduce shape-shifters into the Seers Guild world with this release. You have some new characters to meet (including the mysterious ex, Marina), some secrets of the Codex itself will be revealed, and Mack is going to get into a whole new mess of trouble! This second installment has more action, more magic, more pages, and possibly more bourbon (I didn't count the drinks). Trust me, it's even better than the first one!

\r\n

If you pre-ordered the book, you should have it on your reading gizmo already. If you didn't, hit your favorite ebook retailer and grab it today (links below for your convenience)!

\r\n

I'm currently hard at work writing Summoning Courage: The Piero Codex Book Three to wrap up this trilogy. In the meantime, I hope you enjoy reading Shifting Loyalties!

", + "date_published": "2018-06-14T10:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 5, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.article", + "pk": 29, + "fields": { + "title": "Where the heck is book three?", + "slug": "where-the-heck-is-book-three", + "status": "usable", + "site": 1, + "description": "", + "share_image": null, + "base_template": "", + "content_template": "", + "body": "

I know what you're thinking. Didn't Vince say he was going to give me the conclusion to The Piero Codex trilogy in July? Where is it already?

\r\n

Well, have you ever heard that quote about where the best laid plans often go? Yeah. Summoning Courage WAS planned to ship in July, but as it stands today, the draft is only about half complete. I'm working on it and making steady progress, but the muse and I are having disagreements that have slowed things down. Give me another couple of months to iron out the kinks, m'kay?

\r\n

But I would hate for you to have nothing to read for all that time! If your \"to read\" queue is looking a little dry, let me give you a recommendation to tide you over. Since you enjoy my books, I feel pretty confident that you will love Dana Cameron's Seven Kinds of Hell, the first book in her Fangborn series. It has a lot of the elements you love: dark magics, secret societies, ancient relics, and a fresh take on the \"vampires and werewolves\" concept. I encourage you to check it out!

", + "date_published": "2018-07-25T10:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 5, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.article", + "pk": 30, + "fields": { + "title": "2018 NaNoWriMo After-Action Report and Annual Review", + "slug": "2018-nanowrimo-after-action-report-and-annual-revi", + "status": "usable", + "site": 1, + "description": "", + "share_image": 6, + "base_template": "", + "content_template": "", + "body": "

So much can change in a year!

\r\n

This has been a year of massive change for me, and we'll discuss that below. But first, I participated in National Novel Writing Month again this year, and as last year, this is my report on how I performed. Short story: I didn't \"win\" by writing fifty thousand words, but I did make a very good showing. Here's my word count graph for 2018.

\r\n

\"2018

\r\n

As you can see, my NaNoWriMo project capped out at 31,168 words. Not bad. I drafted twenty-one days in November, so my average was below the required \"winning\" pace. Still, I averaged a thousand words per calendar day, and I feel pretty good about that.

\r\n

2018: The Big Year

\r\n

I started 2018 by getting laid off from my job. Most people consider that a big \"suck\" life event, but for me, it was kind of a relief. I had worked there more than a decade, and over the last 3 years there, massive changes at the company had sucked away most of the joy of my job. I was tired of it, and I needed a change.

\r\n

I found myself in a very lucky position. I had a severance package and resources of my own, so I didn't need to go find a job immediately. I was burned out on the corporate world and could not face another \"enterprise IT\" job. I decided instead that I would try to live the dream, and work on my author career full time for a while.

\r\n

The first four months of the year were extremely productive. I had no day job to interfere with my writing. I did my edits and rewrites on Cursing Fate: The Piero Codex Book One in January. In February, I picked up my half-finished draft of Shifting Loyalties: The Piero Codex Book Two. I tried, as an experiment, tracking words on edits and rewrites, but in practice I discovered that it threw my numbers way off. Although I recorded 41k words in February, I only actually produced around 8,000 words of draft. The rest was pre-existing draft that was edited. Live and learn.

\r\n

With my outline newly revitalized, I dove into drafting Book Two, and finished it in mid-April. During those months, I also registered an LLC, established relationships with the online ebook retailers, ordered ebook covers for the series, and generally got my publishing business ready for action. I ambitiously created a release plan for the series to release one a month, scheduled the first release for may, and started working on Summoning Courage: The Piero Codex Book Three.

\r\n

Nothing Is Ever Easy

\r\n

And then reality happened. Book Three fought me from the very beginning. I was never satisfied with the outline, and the early draft just was not working. I got Book One published in May. Took a break from the draft to do my final revisions on Book Two, which turned out great, except that most of my readers didn't get to see that final polish because Amazon and I had a misunderstanding about publishing dates and final copies. That frustrated me even more, and made it that much harder to get Book Three moving in the right direction.

\r\n

As had happened with the first two books, I reached a point near the middle of book three when I realized the outline needed a complete overhaul, and I was not going to hit my ship dates. That major slowdown in the May-June-July timeframe was me struggling to fix the outline and get back on track.

\r\n

I finally did reach an outline that I could live with, and started rebuilding momentum. This was made difficult by the fact that I was starting to run low on money and started looking for a day job I would not hate. Not an easy task. I was dividing my time between writing, job hunting, and working to level up my software development skills (in aid of the job hunt).

\r\n

I finished the draft of Book Three on the last day of September, just under the wire to meet my revised Q3 delivery target.

\r\n

The draft was very rough, and I knew it needed a lot of revision before it could be published. I had to make a choice. If I worked on the revisions in October, they would drag into November, and I would be completely unprepared for NaNoWriMo. I would not be able to participate, and that idea made me sad.

\r\n

The book was already late, and my twenty or so readers knew that. I shrugged, said what the hell, and put Book Three on a shelf to spend October preparing for NaNo.

\r\n

How I \"Failed\" at NaNoWriMo

\r\n

I had already selected a new character and story for my next project. I knew from experience that I needed a strong outline if I was going to hit high word counts on my first draft, so I spent most of October filling whiteboards with plot points and character arcs. I worked HARD for two weeks. I spent no time drafting, only outlining. I got to a point where I had an outline I considered draftable.

\r\n

I would like to have spent two more weeks on the outline. The first half of the outline was solid and complete, sixteen strong scenes and two that needed a little TLC. The second half had a lot of scenes with vague or missing turning points, and a few that remained one-liners. Unfortunately (or fortunately), my job hunt came to an end, and I found myself with a full time day job again.

\r\n

Starting the new job didn't just consume my daytime hours. The commutes were long. I had to worry about planning meals again, instead of just walking to the fridge. I was learning a new codebase, learning to work with a new team, using a new process, and trying to make a good impression. It was exhausting. And the new hours disrupted my sleep schedule, leaving me even more tired.

\r\n

For the first couple of weeks of November, I pushed myself hard, and I performed well. But I could see that my words per hour were on the low side. Extra hours on the weekends kept me in the race. My outline was holding up. But I wasn't. The change of weather was giving me headaches and nosebleeds. Then I got food poisoning, which lost me a whole day and night.

\r\n

By the week of Thanksgiving, the lack of light, the lack of sleep, and the stress caught up with me. I was killing myself, and I had to ask myself why. At this point, I had proven that I could win NaNo. I had proven three times over that I could complete a novel. I had even proven that I could sell copies books I had written. I did not need to sacrifice my physical or emotional health to prove anything.

\r\n

So I backed off the writing and started working on recovering. It's mid-December and I still don't have my sleep schedule sorted out, but I am less sleep-deprived than I was, and on the way to better. I've put this year's NaNo project on the shelf for a little while. Over Christmas break, I will pick up Summoning Courage and complete the revisions on that. Then, I will finish the draft for my new book, hopefully by late January or early February of 2019.

\r\n

2018 Lessons Learned

\r\n

It was a big year, and I have a lot of lessons learned from it. Here's a summary.

\r\n

Writing fiction is harder than writing code.

\r\n

I'm an introvert. People wear me out, and I need alone-time to recharge. Writing code is something I can do all day. It's like solving a jigsaw puzzle. No people involved. Not very taxing emotionally.

\r\n

Writing fiction, on the other hand, is like a family argument times five. Creating conversations and interactions where I have to carry both sides, to flesh out the emotional reactions of multiple characters, some (most) of whom are much more intense than me. I find it utterly draining. Despite the fact that I had all day to write for much of the year, I seldom spent more than two hours a day actually drafting. Any more than that, and I found I needed time off to recover.

\r\n

Conclusions from that? For starters, full time writer may not be the best career for me. That doesn't mean I'm going to stop writing. Far from it. But I don't see myself being one of those prolific authors who publish a book a month. I'll be happy to produce two books a year for now.

\r\n

Sleep is important. Health in general is important.

\r\n

If I'm tired or depressed, I simply can't produce good fiction. Creativity requires vitality, energy, desire. To keep myself writing, I have to accept my limitations and take care of myself first. And that's something I have struggled with all my life. Well, that's how it goes. Keep struggling, Vince!

\r\n

I make plot-driven outlines, and character-heavy drafts.

\r\n

My process is very plot-driven. I create an outline marking the events of the external story. Then when I draft it, I internalize the characters, dramatize their internal reactions. I'm not particularly good at characters. It's hard work. I enjoy outlining more, because it's less like an awkward cocktail party and more like architecting software. (What can I say, I'm weird.)

\r\n

Maybe I should find a writing partner who is all about character and hates plotting? I bet we could produce some great stuff together! But I really need to level up my skills a lot more before I'm ready for that kind of collaboration.

\r\n

I still don't understand the \"middle build\".

\r\n

When it comes to story structure, I feel like I have a solid grasp of Act One, the first quarter of the story. I think I do alright with that last quarter, which I tend to write as a fast-paced rush to the climax. But the middle still baffles me. I'm only just beginning to understand how to structure progressive plot complications and character arcs to fill the middle half of the story with compelling reading.

\r\n

As a result, I think my next project I will spend even more time outlining. And I will spend more time analyzing great stories to learn from them.

\r\n

NaNoWriMo 2019 and Beyond

\r\n

After this year's experience, I'm not sure I'll be participating in NaNo next year. I no longer feel I have anything to prove, and the event is inconveniently timed for me. It's the time of year I prefer to be ramping down, not ramping up.

\r\n

I think instead I will focus on honing my skills and creating books at a pace I know is sustainable for me. And I will also allow myself to spend time on other, non-book side projects again. Because I like writing software too!

", + "date_published": "2018-12-16T11:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 4, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.article", + "pk": 31, + "fields": { + "title": "How I Write a Novel: My Creative Process as of 2019", + "slug": "how-i-write-a-novel-my-creative-process-as-of-2019", + "status": "usable", + "site": 1, + "description": "", + "share_image": null, + "base_template": "", + "content_template": "", + "body": "

Two years ago I wrote a post about Creating a Creative Process. Since then, I have completed three novels and am in production on a fourth, with many more in the pipeline. I have learned a lot of lessons through trial and error. I still have a lot more to learn. But after two years, I figured it was time for an update.

\r\n

Note that this post is not titled \"How To Write a Novel\". This is a snapshot of my own process at a specific time in my writing journey. Maybe it will be helpful to other writers, but it's not a prescription nor a recommendation. I'm just sharing my experience.

\r\n

Step 0. From Idea To Project: The Gestational Phase

\r\n

Like every writer, I have a huge pile of ideas I have jotted down over the years. Every project begins as at least one idea. But most ideas never go into projects. I like to pull elements out of stories I like, or out of stories I don't like that I think I could tell better. I spend a lot of time mulling, recombining pieces to see what could be constructed from them.

\r\n

An idea doesn't turn into a project until I have nailed down three important aspects of the story:

\r\n
    \r\n
  1. The Premise (Why): What's the thing that makes it worth my time to write?
  2. \r\n
  3. The Genre (What): I will never again begin earnest work on a project without identifying the genre I'm targeting. I currently use the Story Grid definitions of Genre, but I also consult Save the Cat's ten types of stories, Amazon categories, and other definitions of genre. This defines what kind of story I want to tell, and creates some constraints on structure and conventions.
  4. \r\n
  5. The Main Character (Who): I need to have at least one of the POV characters \"figured out\" before I can begin. They don't need to be fully fleshed out, but I need to have a sense of their personality and emotional journey, or else anything I write is going to sound flat and bland (I know, I tried it).
  6. \r\n
\r\n

This phase has no time bound. For some projects it is infinite, for others, a few months. Years will not be unusual, because I can only work so many projects in a year.

\r\n

Step 1. The Board: The Story Forming Phase (2-8 weeks)

\r\n

I have a confession that may shock and dismay. I don't enjoy drafting. Drafting is the \"writing\" part of writing, where you type out all those damn words. I don't hate drafting, but I don't like it. To me, drafting is the scut work, the drudgery that simply must be done to produce the product. Some writers love drafting, and their process turns entirely around that activity. They write a draft, and then revise it with further drafts three or five or seven times. My process is designed to keep drafting to an absolute minimum. I don't want to write anything more than once!

\r\n

 

\r\n
\"brief
\r\n

SPOILER ALERT! \"The Board\" for my current work in progress.

\r\n

 

\r\n

Therefore, step one is to create an outline. There are as many ways to create an outline as there are outliners. I have adopted a slightly modified version of The Board as described in Save the Cat. The story is laid out in a grid with four rows, each representing 25% of the story. In my own head I refer to these as \"acts\" but that tends to confuse (or anger!) people who cling religiously to the Three Act Structure, so I'll just call them quarters.

\r\n

This grid layout is an excellent visualization for story structure, because the most significant events of the story will form a nice tidy column down the right side of the grid (these are the \"plot points\" or major turns and the climax). The second-tier events of the story (the \"pinch points\") will likewise be lined up down the middle of the grid. The setup work will be lined up in the left column. The spaces between are the connective tissue of the story. This gives a story that is well-paced. Snyder recommends doing this with notecards. I have been writing them on a whiteboard. It's the layout, not the medium, that matters.

\r\n

What I do at the board is what many writers do in their first and second draft. I figure out what happens, and why, and to whom. I typically work from right to left. What are the big events and major turns? What are the complications that lead up to them? What setup do I need to get there? I decompose the story into acts, sequences, and scenes. In the beginning it's a lot of scribbling, but before long it coalesces into a solid outline.

\r\n

At the end of this process, I have enumerated all the major characters and turned my general idea into a list of scenes that should flow logically from beginning to end.

\r\n

It's difficult to say how long this phase truly lasts, since I have only recently adopted this technique. For my current work in progress, I spent two full weeks working on it a couple of hours a day, so maybe thirty working hours? But that plot was already well-developed in my head before I began outlining. And in future I intend to be even more thorough in this phase, so probably longer. I imagine some projects will require eighty hours or more.

\r\n

Step 2. The Half Draft (4-8 weeks)

\r\n

Now begins the drafting phase. I begin to draft the scenes, one at a time, from beginning to end, in order. I will explore the inner lives of the characters, add color and personality quirks, invent settings, develop the voices, etc.

\r\n

In this step I will draft somewhere between 25% to 50% of the words. And then I will stop.

\r\n

By this point, I will have discovered several problems with my original outline. Maybe I need an additional scene to drive the motivation of a character. Maybe I missed an obvious \"why don't they just\" solution to the character's problem that needs to be explained away or blocked by new complications. Maybe I discover that a planned solution to a problem won't actually work and have to resolve that.

\r\n

Probably I have added several new setups during drafting whose payoffs will need to be added to the outline. Conflict threads will have developed that will need to be wrapped up somehow at the story's conclusion. The exact order of events may shift.

\r\n

In short, before I can finish the draft, I need to go back and revise my outline.

\r\n

The half-draft typically takes 1-2 months at my current drafting pace.

\r\n

Step 3. Back To The Board (2-8 weeks)

\r\n

I put the draft away for a time and return to my outline. I have a list of problems to sort out, some gaps to fill, some weak scenes to prop up, replace, or remove. The climax, which has been somewhat abstract, now begins to take on very specific forms.

\r\n

I bang my head against the board until I'm satisfied that the story is right.

\r\n

This step may last a few weeks or a few months, depending how badly I have botched the original outline, or how tight the corner is that I have painted myself into.

\r\n

Step 4. The \"First\" Draft (4-8 weeks)

\r\n

Having revised my outline, I return to drafting. If necessary, I will perform some revision on existing scenes from the Half Draft, but more typically I will simply make notes about changes that need to be made and continue from where I left off, drafting the scenes in order until I complete the draft.

\r\n

I label this the First Draft, but note that by this point the story has been revised multiple times already. This is where many writers (pantsers) will be on a Third Draft or later.

\r\n

Finishing the draft takes 1-2 months.

\r\n

Step 5. Have a Drink: The Fallow Period (2-8 weeks)

\r\n

I have a special bottle of bourbon that I only open when I hit a writing milestone. Finishing a First Draft is one such milestone, so I pour one, make toast to my hard work, and enjoy the feeling of being done with something big.

\r\n

Then, I put the draft on the shelf for a while and think about something else.

\r\n

This step may last a couple of weeks, or a couple of months, depending what else I have going on, and how tired I am of looking at the pages!

\r\n

Step 6: The Read Through (1 week)

\r\n

Next, I pick up the draft again and simply read it through, from beginning to end. I make notes of any issues I find: plot holes, character inconsistencies, poorly worded passages, lame metaphors, etc. I explicitly do not attempt to fix any of these issues in this step, only identify them.

\r\n

When I say that I make notes, I'm talking specifically about Scrivener's \"comments\" feature that allows you to attach a comment to any arbitrary selection of text. Those comments become my To-Do list in the revision stage.

\r\n

I also leave comments when I'm drafting, because struggling to find the right word in the moment often becomes a form of resistance. Drafting notes I leave myself are things like [describe clothing] and [opposite of steep?].

\r\n

My books are not terribly long, to date, so the read through usually takes just a few days.

\r\n

Step 7. The First Revision (1-4 weeks)

\r\n

Next I work my way through the book, finding all the annotated passages (the Scrivener comments), and correcting the issues one at a time. I tend to write short in my first draft, so this almost always involves adding words to the draft, not trimming.

\r\n

Depending on how many problems I found, the first revision may take a week or up to a month.

\r\n

Step 8. Other Eyes (2-4 weeks)

\r\n

At this point I have made the project the best I can make it by myself. Time to ship it off to the editor and/or beta readers and see what they think.

\r\n

I will get back a list of notes and suggestions, which I review. I need to decide first, whether the issue is really a problem. Second, is it something that I believe I should fix? I'm unlikely to embark on a total rewrite or major plot change at this stage. Third, how should I fix it? I make more notes.

\r\n

Step 9. The Final Revision. (1-4 weeks)

\r\n

As in Step 7, I work through the notes one by one until they are all fixed.

\r\n

Again, the final revision may take a week or a month depending on the number of issues to be corrected.

\r\n

Step 10. The Polish (1-2 weeks)

\r\n

The Polish step is where the manuscript gets cleaned up and turned into a product. There are a few processes I go through to polish, all of them pretty simple.

\r\n\r\n

This step doesn't require much creative work, and it typically takes two weeks, but can be done in one week if I have enough free time.

\r\n

Have Another Drink

\r\n

At this point, the creative process is done, and the business process is just beginning. I have another drink of my special bourbon to celebrate a completed work, publish that puppy, and do whatever is next.

\r\n

That's the full skinny on my creative process, as of the beginning of 2019. As you can see, it has evolved a great deal over two years of steady writing. I know the process will continue to evolve as I level up and discover new refinements.

\r\n

I hope that some other writers find this helpful, and that curious readers now have a little better understanding of what goes into producing a novel. Let me know what you think!

", + "date_published": "2019-01-08T11:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 4, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.article", + "pk": 32, + "fields": { + "title": "Summoning Courage Is Available Today!", + "slug": "summoning-courage-is-available-today", + "status": "usable", + "site": 1, + "description": "", + "share_image": 8, + "base_template": "", + "content_template": "commoncontent/blocks/article_featuredimage.html", + "body": "

According to Hofstadter's Law, every project takes longer than you expect it to (even when you take into account Hofstadter's Law), and my latest book project is no exception. But, as of today, Summoning Courage: The Piero Codex Book Three is available at ebook retailers everywhere!

\r\n

I am delighted to announce the release of this conclusion to The Piero Codex trilogy. These were my first novels, and it has been an incredible learning experience for me to produce them. Those lessons learned will make my next series even better.

\r\n

Now that the edits are done and the cover is attached, I'm going to take a little break from writing to try to close out another very late project: the remodel of Albatross House. I'm going to stay focused on that project until it's done, so I probably will not be releasing another book in 2019. However, I have about 80 thousand words already banked for my next series; there will definitely be another release in the future!

\r\n

Thank you for following along with my creative journey. I sincerely hope you are enjoying it!

\r\n

Your author friend, Vince Veselosky

", + "date_published": "2019-04-30T10:00:00Z", + "date_modified": null, + "expires": null, + "_order": 1, + "seo_title": "", + "seo_description": "", + "custom_copyright_notice": "", + "custom_icon": "", + "locale": "en_US", + + "section": 5, + "image_set": [], + "attachment_set": [] + } + }, + { + "model": "commoncontent.menu", + "pk": 1, + "fields": { + "site": 1, + "admin_name": "Main Nav", + "slug": "main-nav", + "title": "" + } + }, + { + "model": "commoncontent.link", + "pk": 1, + "fields": { + "menu": 1, + "url": "/", + "title": "Home", + "custom_icon": "", + "description": "", + "share_image": null + } + }, + { + "model": "commoncontent.link", + "pk": 2, + "fields": { + "menu": 1, + "url": "/seers-guild/", + "title": "Seers Guild", + "custom_icon": "", + "description": "", + "share_image": null + } + }, + { + "model": "commoncontent.link", + "pk": 3, + "fields": { + "menu": 1, + "url": "/vip-list/", + "title": "VIP List", + "custom_icon": "", + "description": "", + "share_image": null + } + }, + { + "model": "redirects.redirect", + "pk": 1, + "fields": { + "site": 1, + "old_path": "/seers-guild/", + "new_path": "https://books2read.com/rl/seers-guild" + } + }, + { + "model": "redirects.redirect", + "pk": 2, + "fields": { + "site": 1, + "old_path": "/vip-list/", + "new_path": "https://www.subscribepage.com/veselosky-vip-list" + } + }, + { + "model": "sites.site", + "pk": 1, + "fields": { + "domain": "vince.veselosky.me", + "name": "Vince Veselosky" + } + }, + { + "model": "sites.site", + "pk": 2, + "fields": { + "domain": "www.mindvessel.com", + "name": "Mindvessel" + } + }, + { + "model": "sites.site", + "pk": 3, + "fields": { + "domain": "www.iqlovecraft.com", + "name": "I.Q. Lovecraft" + } + } +] diff --git a/storyville/settings.py b/storyville/settings.py index 58b7a6c..2641f73 100644 --- a/storyville/settings.py +++ b/storyville/settings.py @@ -11,10 +11,12 @@ See also https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ """ + +from importlib.util import find_spec from pathlib import Path +import commoncontent.apps import environ -import genericsite.apps PROJECT = __name__.split(".")[0] # Build paths inside the project like this: BASE_DIR / 'subdir'. @@ -27,10 +29,29 @@ WSGI_APPLICATION = f"{PROJECT}.wsgi.application" ROOT_URLCONF = f"{PROJECT}.urls" -INSTALLED_APPS = genericsite.apps.plus( - # Third party apps: +INSTALLED_APPS = [ + "storyville", # app for project-specific stuff + # commoncontent apps for static site generation + *commoncontent.apps.CONTENT, + # commoncontent apps for publishing tools "django_extensions", -) + "tinymce", + # contrib apps required by commoncontent for statics + "django.contrib.contenttypes", + "django.contrib.humanize", + "django.contrib.redirects", + "django.contrib.sitemaps", + "django.contrib.sites", + "django.contrib.staticfiles", + # contrib apps required by commoncontent for dynamically served apps + "django.contrib.auth", + "django.contrib.messages", + "django.contrib.sessions", + # Optional admin with commoncontent extensions + "django.contrib.admin", + "django.contrib.admindocs", + "sitevars", +] MIDDLEWARE = [ # https://docs.djangoproject.com/en/4.2/ref/middleware/#django.middleware.security.SecurityMiddleware @@ -44,7 +65,7 @@ "django.middleware.clickjacking.XFrameOptionsMiddleware", # Set request.site by checking for a Site where domain is the host header "django.contrib.sites.middleware.CurrentSiteMiddleware", - "genericsite.redirects.TemporaryRedirectFallbackMiddleware", + "commoncontent.redirects.TemporaryRedirectFallbackMiddleware", ] TEMPLATES = [ @@ -60,7 +81,8 @@ "django.contrib.messages.context_processors.messages", "django.template.context_processors.media", "django.template.context_processors.static", - "genericsite.apps.context_defaults", + "commoncontent.apps.context_defaults", + "sitevars.context_processors.inject_sitevars", ], }, }, @@ -71,7 +93,6 @@ LANGUAGE_CODE = "en-US" TIME_ZONE = "America/New_York" USE_I18N = True -USE_L10N = True USE_TZ = True # Default primary key field type @@ -145,24 +166,21 @@ EMAIL_CONFIG = env.email_url("EMAIL_URL", default="consolemail://") vars().update(EMAIL_CONFIG) - -# CELERY settings -# If the environment has not provided settings, assume there is no broker -# and run celery tasks in-process. This means you MUST provide -# CELERY_TASK_ALWAYS_EAGER=False in your environment to actually use celery. -CELERY_TASK_ALWAYS_EAGER = env("CELERY_TASK_ALWAYS_EAGER", default=True) -CELERY_TASK_EAGER_PROPAGATES = env("CELERY_TASK_EAGER_PROPAGATES", default=True) -# For development setup, assume default of local redis. -CELERY_BROKER_URL = env("CELERY_BROKER_URL", default="redis://localhost:6379/1") -CELERY_RESULT_BACKEND = env("CELERY_RESULT_BACKEND", default="") -CELERY_TIME_ZONE = TIME_ZONE -try: - import django_celery_beat - +if find_spec("celery"): + # CELERY settings + # If the environment has not provided settings, assume there is no broker + # and run celery tasks in-process. This means you MUST provide + # CELERY_TASK_ALWAYS_EAGER=False in your environment to actually use celery. + CELERY_TASK_ALWAYS_EAGER = env("CELERY_TASK_ALWAYS_EAGER", default=True) + CELERY_TASK_EAGER_PROPAGATES = env("CELERY_TASK_EAGER_PROPAGATES", default=True) + # For development setup, assume default of local redis. + CELERY_BROKER_URL = env("CELERY_BROKER_URL", default="redis://localhost:6379/1") + CELERY_RESULT_BACKEND = env("CELERY_RESULT_BACKEND", default="") + CELERY_TIME_ZONE = TIME_ZONE + +if find_spec("django_celery_beat"): CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler" INSTALLED_APPS.append("django_celery_beat") -except ImportError: - pass # Use rich logging for pretty console logs @@ -200,11 +218,7 @@ # SECTION 2: App configuration. ####################################################################################### -THUMBNAIL_PROCESSORS = genericsite.apps.THUMBNAIL_PROCESSORS -THUMBNAIL_WIDGET_OPTIONS = genericsite.apps.THUMBNAIL_WIDGET_OPTIONS -THUMBNAIL_DEBUG = DEBUG - -TINYMCE_DEFAULT_CONFIG = genericsite.apps.TINYMCE_CONFIG +TINYMCE_DEFAULT_CONFIG = commoncontent.apps.TINYMCE_CONFIG ####################################################################################### # SECTION 3: DEVELOPMENT: If running in a dev environment, loosen restrictions @@ -222,15 +236,11 @@ # the DEBUG section. See README for details. # SITE_ID = 1 - try: - import debug_toolbar - + if find_spec("debug_toolbar"): + # Debug toolbar is optional INSTALLED_APPS.append("debug_toolbar") MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware") INTERNAL_IPS = [ "127.0.0.1", ] # See also urls.py for debug_toolbar urls - except ImportError: - # Dev tools are optional - pass diff --git a/storyville/urls.py b/storyville/urls.py index 12372bd..69228db 100644 --- a/storyville/urls.py +++ b/storyville/urls.py @@ -13,18 +13,33 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ + +from importlib.util import find_spec + +from commoncontent.sitemaps import sitemaps +from commoncontent.views_optional import TinyMCEImageListView from django.conf import settings from django.contrib import admin +from django.contrib.sitemaps.views import sitemap from django.urls import include, path -from genericsite import views as generic urlpatterns = [ - path("accounts/profile/", generic.ProfileView.as_view(), name="account_profile"), path("accounts/", include("django.contrib.auth.urls")), path("admin/doc/", include("django.contrib.admindocs.urls")), path("admin/", admin.site.urls), + path( + "images/recent.json", + TinyMCEImageListView.as_view(), + name="tinymce_image_list", + ), path("tinymce/", include("tinymce.urls")), - path("", include("genericsite.urls")), + path( + "sitemap.xml", + sitemap, + {"sitemaps": sitemaps}, + name="django.contrib.sitemaps.views.sitemap", + ), + path("", include("commoncontent.urls")), ] if settings.DEBUG: @@ -36,10 +51,7 @@ # urlpatterns += staticfiles_urlpatterns() # automatic when DEBUG on urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) - try: - import debug_toolbar - + # Add Django Debug Toolbar if installed + if find_spec("debug_toolbar"): # Catch-all patterns may block these if appended, hence insert - urlpatterns.insert(0, path("__debug__/", include(debug_toolbar.urls))) - except ImportError: - pass + urlpatterns.insert(0, path("__debug__/", include("debug_toolbar.urls"))) diff --git a/templates/genericsite/blocks/header_masthead.html b/templates/genericsite/blocks/header_masthead.html deleted file mode 100644 index 3015a69..0000000 --- a/templates/genericsite/blocks/header_masthead.html +++ /dev/null @@ -1,14 +0,0 @@ -
{% load genericsite i18n %} -

{% firstof brand request.site.name %}

- {% if tagline %}

{{ tagline }}

{% endif %} - {% menu "main-nav" as menu %} - -
\ No newline at end of file diff --git a/templates/iqlovecraft/index.html b/templates/iqlovecraft/index.html index 505e067..bd982e0 100644 --- a/templates/iqlovecraft/index.html +++ b/templates/iqlovecraft/index.html @@ -1,32 +1,32 @@ -{% extends 'genericsite/base.html' %}{% load static %} - +{% extends 'commoncontent/base.html' %} +{% load static %} {% block precontent %} -
- -
-
-

Welcome Readers!

-

Welcome to the online home of I.Q. Lovecraft! I spin tales of supernatural - erotic situations, exploring pansexuality, polyamory, and diverse sexual - practices.

+
+ -
-

Become a Patron

- - Patreon logo - -

Patrons get a sexy new chapter of work in progress every week, bonus scenes, - FREE books, and more! -

-

Become a Patron

- +
+
+

Welcome Readers!

+

+ Welcome to the online home of I.Q. Lovecraft! I spin tales of supernatural + erotic situations, exploring pansexuality, polyamory, and diverse sexual + practices. +

+
+
+

Become a Patron

+ + Patreon logo + +

+ Patrons get a sexy new chapter of work in progress every week, bonus scenes, + FREE books, and more! +

+

+ Become a Patron +

+
-
{% endblock precontent %} diff --git a/templates/storyville/blocks/header_masthead.html b/templates/storyville/blocks/header_masthead.html new file mode 100644 index 0000000..049bdf9 --- /dev/null +++ b/templates/storyville/blocks/header_masthead.html @@ -0,0 +1,27 @@ +
+ {% load commoncontent i18n %} +

+ {% firstof brand request.site.name %} +

+ {% if tagline %}

{{ tagline }}

{% endif %} + {% menu "main-nav" as menu %} + +
diff --git a/templates/veselosky.me/blocks/novels_card.html b/templates/veselosky.me/blocks/novels_card.html index 2d5c03e..d088efe 100644 --- a/templates/veselosky.me/blocks/novels_card.html +++ b/templates/veselosky.me/blocks/novels_card.html @@ -1,13 +1,30 @@ {% load static %} -
+
-

For readers: Read my urban fantasy series, The Piero Codex

+

+ For readers: Read my urban fantasy series, The Piero Codex +

    -
  1. Cursing Fate book cover
  2. -
  3. Cursing Fate book cover
  4. -
  5. Cursing Fate book cover
  6. +
  7. + + Cursing Fate book cover + +
  8. +
  9. + + Cursing Fate book cover + +
  10. +
  11. + + Cursing Fate book cover + +
-
\ No newline at end of file +
diff --git a/templates/veselosky.me/index.html b/templates/veselosky.me/index.html index 8d1aed9..fd84172 100644 --- a/templates/veselosky.me/index.html +++ b/templates/veselosky.me/index.html @@ -1,20 +1,15 @@ -{% extends 'genericsite/base.html' %} - +{% extends 'commoncontent/base.html' %} {% block header %} - + {% endblock header %} - {% block precontent %} -
- {% include "veselosky.me/blocks/novels_card.html" %} -
+
{% include "veselosky.me/blocks/novels_card.html" %}
{% endblock precontent %} - {% block content %} -
-

Latest Articles

-
-{{ block.super }} -{% endblock content %} \ No newline at end of file +
+

Latest Articles

+
+ {{ block.super }} +{% endblock content %} diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_general.py b/tests/test_general.py new file mode 100644 index 0000000..4fcb958 --- /dev/null +++ b/tests/test_general.py @@ -0,0 +1,38 @@ +""" +"Smoke" tests to ensure that views load and templates render. +""" + +from django.test import TestCase, override_settings +from django.urls import reverse + + +@override_settings(SITE_ID=1) +class SmokeTests(TestCase): + fixtures = ["smoketest.json"] + + def test_homepage(self): + response = self.client.get(reverse("home_page")) + self.assertEqual(response.status_code, 200) + # Custom home page for the site + self.assertTemplateUsed(response, "veselosky.me/index.html") + # Header template set by sitevar + self.assertTemplateUsed(response, "storyville/blocks/header_masthead.html") + # Filename is veseloksy.css, but it's delivered as veselosky..css + self.assertContains( + response, 'link rel="stylesheet" href="/static/veselosky.me/veselosky.' + ) + + def test_sitemap(self): + response = self.client.get(reverse("django.contrib.sitemaps.views.sitemap")) + self.assertEqual(response.status_code, 200) + + def test_rss_feed(self): + response = self.client.get(reverse("site_feed")) + self.assertEqual(response.status_code, 200) + + def test_tinymce_image_list(self): + response = self.client.get(reverse("tinymce_image_list")) + self.assertEqual(response.status_code, 200) + imglist = response.json() + # 8 images in the fixture, view should return up to 10 + self.assertEqual(len(imglist), 8) diff --git a/tox.ini b/tox.ini index 75c9a61..0fdc757 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,6 @@ [tox] -# Django 4.2 supports Python 3.8 and later. Target 3.10 for deployment (Ubuntu 22.04). envlist = - {py38,py39,py310,py311}-django42 + {py38,py39,py310,py311,py312,py313}-django42 py310-checks skipsdist = true skip_missing_interpreters = true @@ -14,7 +13,7 @@ commands = python manage.py collectstatic --noinput python -Wa manage.py test {posargs} deps = - -r requirements.txt + -r pyproject.toml [testenv:py310-checks] basepython=python3.10 @@ -22,18 +21,18 @@ commands = ; Check model consistency and other bugs python manage.py check ; Check templates for syntax errors - python manage.py validate_templates --ignore-app genericsite + python manage.py validate_templates --ignore-app commoncontent ; Check whether you forgot to run makemigrations after changing models python manage.py makemigrations --no-input --dry-run --check deps = - -r requirements.txt + -r pyproject.toml [testenv:py310-coverage] basepython=python3.10 commands = coverage run --source='.' manage.py test {posargs} deps = - -r requirements.txt + -r pyproject.toml coverage [coverage:run] @@ -59,3 +58,5 @@ python = 3.9: py39 3.10: py310 3.11: py311 + 3.12: py312 + 3.13: py313 diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..433fa37 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1106 @@ +version = 1 +requires-python = ">=3.10" + +[[package]] +name = "asgiref" +version = "3.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590", size = 35186 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828 }, +] + +[[package]] +name = "asttokens" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918 }, +] + +[[package]] +name = "certifi" +version = "2024.8.30" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/8b/825cc84cf13a28bfbcba7c416ec22bf85a9584971be15b21dd8300c65b7f/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", size = 196363 }, + { url = "https://files.pythonhosted.org/packages/23/81/d7eef6a99e42c77f444fdd7bc894b0ceca6c3a95c51239e74a722039521c/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", size = 125639 }, + { url = "https://files.pythonhosted.org/packages/21/67/b4564d81f48042f520c948abac7079356e94b30cb8ffb22e747532cf469d/charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", size = 120451 }, + { url = "https://files.pythonhosted.org/packages/c2/72/12a7f0943dd71fb5b4e7b55c41327ac0a1663046a868ee4d0d8e9c369b85/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", size = 140041 }, + { url = "https://files.pythonhosted.org/packages/67/56/fa28c2c3e31217c4c52158537a2cf5d98a6c1e89d31faf476c89391cd16b/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", size = 150333 }, + { url = "https://files.pythonhosted.org/packages/f9/d2/466a9be1f32d89eb1554cf84073a5ed9262047acee1ab39cbaefc19635d2/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", size = 142921 }, + { url = "https://files.pythonhosted.org/packages/f8/01/344ec40cf5d85c1da3c1f57566c59e0c9b56bcc5566c08804a95a6cc8257/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", size = 144785 }, + { url = "https://files.pythonhosted.org/packages/73/8b/2102692cb6d7e9f03b9a33a710e0164cadfce312872e3efc7cfe22ed26b4/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", size = 146631 }, + { url = "https://files.pythonhosted.org/packages/d8/96/cc2c1b5d994119ce9f088a9a0c3ebd489d360a2eb058e2c8049f27092847/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", size = 140867 }, + { url = "https://files.pythonhosted.org/packages/c9/27/cde291783715b8ec30a61c810d0120411844bc4c23b50189b81188b273db/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", size = 149273 }, + { url = "https://files.pythonhosted.org/packages/3a/a4/8633b0fc1a2d1834d5393dafecce4a1cc56727bfd82b4dc18fc92f0d3cc3/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", size = 152437 }, + { url = "https://files.pythonhosted.org/packages/64/ea/69af161062166b5975ccbb0961fd2384853190c70786f288684490913bf5/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", size = 150087 }, + { url = "https://files.pythonhosted.org/packages/3b/fd/e60a9d9fd967f4ad5a92810138192f825d77b4fa2a557990fd575a47695b/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", size = 145142 }, + { url = "https://files.pythonhosted.org/packages/6d/02/8cb0988a1e49ac9ce2eed1e07b77ff118f2923e9ebd0ede41ba85f2dcb04/charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", size = 94701 }, + { url = "https://files.pythonhosted.org/packages/d6/20/f1d4670a8a723c46be695dff449d86d6092916f9e99c53051954ee33a1bc/charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", size = 102191 }, + { url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 }, + { url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 }, + { url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 }, + { url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 }, + { url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 }, + { url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 }, + { url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 }, + { url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 }, + { url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 }, + { url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 }, + { url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 }, + { url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 }, + { url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 }, + { url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 }, + { url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 }, + { url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445 }, + { url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275 }, + { url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020 }, + { url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128 }, + { url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277 }, + { url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174 }, + { url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838 }, + { url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149 }, + { url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043 }, + { url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229 }, + { url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556 }, + { url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772 }, + { url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 }, + { url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 }, + { url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 }, + { url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 }, + { url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 }, + { url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 }, + { url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 }, + { url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 }, + { url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 }, + { url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 }, + { url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 }, + { url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 }, + { url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 }, + { url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 }, + { url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 }, + { url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 }, + { url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 }, + { url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 }, + { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, +] + +[[package]] +name = "click" +version = "8.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, +] + +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "coverage" +version = "7.6.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/d2/c25011f4d036cf7e8acbbee07a8e09e9018390aee25ba085596c4b83d510/coverage-7.6.9.tar.gz", hash = "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d", size = 801710 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/f3/f830fb53bf7e4f1d5542756f61d9b740352a188f43854aab9409c8cdeb18/coverage-7.6.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85d9636f72e8991a1706b2b55b06c27545448baf9f6dbf51c4004609aacd7dcb", size = 207024 }, + { url = "https://files.pythonhosted.org/packages/4e/e3/ea5632a3a6efd00ab0a791adc0f3e48512097a757ee7dcbee5505f57bafa/coverage-7.6.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:608a7fd78c67bee8936378299a6cb9f5149bb80238c7a566fc3e6717a4e68710", size = 207463 }, + { url = "https://files.pythonhosted.org/packages/e4/ae/18ff8b5580e27e62ebcc888082aa47694c2772782ea7011ddf58e377e98f/coverage-7.6.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96d636c77af18b5cb664ddf12dab9b15a0cfe9c0bde715da38698c8cea748bfa", size = 235902 }, + { url = "https://files.pythonhosted.org/packages/6a/52/57030a8d15ab935624d298360f0a6704885578e39f7b4f68569e59f5902d/coverage-7.6.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75cded8a3cff93da9edc31446872d2997e327921d8eed86641efafd350e1df1", size = 233806 }, + { url = "https://files.pythonhosted.org/packages/d0/c5/4466602195ecaced298d55af1e29abceb812addabefd5bd9116a204f7bab/coverage-7.6.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7b15f589593110ae767ce997775d645b47e5cbbf54fd322f8ebea6277466cec", size = 234966 }, + { url = "https://files.pythonhosted.org/packages/b0/1c/55552c3009b7bf96732e36548596ade771c87f89cf1f5a8e3975b33539b5/coverage-7.6.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:44349150f6811b44b25574839b39ae35291f6496eb795b7366fef3bd3cf112d3", size = 234029 }, + { url = "https://files.pythonhosted.org/packages/bb/7d/da3dca6878701182ea42c51df47a47c80eaef2a76f5aa3e891dc2a8cce3f/coverage-7.6.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d891c136b5b310d0e702e186d70cd16d1119ea8927347045124cb286b29297e5", size = 232494 }, + { url = "https://files.pythonhosted.org/packages/28/cc/39de85ac1d5652bc34ff2bee39ae251b1fdcaae53fab4b44cab75a432bc0/coverage-7.6.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:db1dab894cc139f67822a92910466531de5ea6034ddfd2b11c0d4c6257168073", size = 233611 }, + { url = "https://files.pythonhosted.org/packages/d1/2b/7eb011a9378911088708f121825a71134d0c15fac96972a0ae7a8f5a4049/coverage-7.6.9-cp310-cp310-win32.whl", hash = "sha256:41ff7b0da5af71a51b53f501a3bac65fb0ec311ebed1632e58fc6107f03b9198", size = 209712 }, + { url = "https://files.pythonhosted.org/packages/5b/35/c3f40a2269b416db34ce1dedf682a7132c26f857e33596830fa4deebabf9/coverage-7.6.9-cp310-cp310-win_amd64.whl", hash = "sha256:35371f8438028fdccfaf3570b31d98e8d9eda8bb1d6ab9473f5a390969e98717", size = 210553 }, + { url = "https://files.pythonhosted.org/packages/b1/91/b3dc2f7f38b5cca1236ab6bbb03e84046dd887707b4ec1db2baa47493b3b/coverage-7.6.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:932fc826442132dde42ee52cf66d941f581c685a6313feebed358411238f60f9", size = 207133 }, + { url = "https://files.pythonhosted.org/packages/0d/2b/53fd6cb34d443429a92b3ec737f4953627e38b3bee2a67a3c03425ba8573/coverage-7.6.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:085161be5f3b30fd9b3e7b9a8c301f935c8313dcf928a07b116324abea2c1c2c", size = 207577 }, + { url = "https://files.pythonhosted.org/packages/74/f2/68edb1e6826f980a124f21ea5be0d324180bf11de6fd1defcf9604f76df0/coverage-7.6.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc660a77e1c2bf24ddbce969af9447a9474790160cfb23de6be4fa88e3951c7", size = 239524 }, + { url = "https://files.pythonhosted.org/packages/d3/83/8fec0ee68c2c4a5ab5f0f8527277f84ed6f2bd1310ae8a19d0c5532253ab/coverage-7.6.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c69e42c892c018cd3c8d90da61d845f50a8243062b19d228189b0224150018a9", size = 236925 }, + { url = "https://files.pythonhosted.org/packages/8b/20/8f50e7c7ad271144afbc2c1c6ec5541a8c81773f59352f8db544cad1a0ec/coverage-7.6.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0824a28ec542a0be22f60c6ac36d679e0e262e5353203bea81d44ee81fe9c6d4", size = 238792 }, + { url = "https://files.pythonhosted.org/packages/6f/62/4ac2e5ad9e7a5c9ec351f38947528e11541f1f00e8a0cdce56f1ba7ae301/coverage-7.6.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4401ae5fc52ad8d26d2a5d8a7428b0f0c72431683f8e63e42e70606374c311a1", size = 237682 }, + { url = "https://files.pythonhosted.org/packages/58/2f/9d2203f012f3b0533c73336c74134b608742be1ce475a5c72012573cfbb4/coverage-7.6.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98caba4476a6c8d59ec1eb00c7dd862ba9beca34085642d46ed503cc2d440d4b", size = 236310 }, + { url = "https://files.pythonhosted.org/packages/33/6d/31f6ab0b4f0f781636075f757eb02141ea1b34466d9d1526dbc586ed7078/coverage-7.6.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee5defd1733fd6ec08b168bd4f5387d5b322f45ca9e0e6c817ea6c4cd36313e3", size = 237096 }, + { url = "https://files.pythonhosted.org/packages/7d/fb/e14c38adebbda9ed8b5f7f8e03340ac05d68d27b24397f8d47478927a333/coverage-7.6.9-cp311-cp311-win32.whl", hash = "sha256:f2d1ec60d6d256bdf298cb86b78dd715980828f50c46701abc3b0a2b3f8a0dc0", size = 209682 }, + { url = "https://files.pythonhosted.org/packages/a4/11/a782af39b019066af83fdc0e8825faaccbe9d7b19a803ddb753114b429cc/coverage-7.6.9-cp311-cp311-win_amd64.whl", hash = "sha256:0d59fd927b1f04de57a2ba0137166d31c1a6dd9e764ad4af552912d70428c92b", size = 210542 }, + { url = "https://files.pythonhosted.org/packages/60/52/b16af8989a2daf0f80a88522bd8e8eed90b5fcbdecf02a6888f3e80f6ba7/coverage-7.6.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:99e266ae0b5d15f1ca8d278a668df6f51cc4b854513daab5cae695ed7b721cf8", size = 207325 }, + { url = "https://files.pythonhosted.org/packages/0f/79/6b7826fca8846c1216a113227b9f114ac3e6eacf168b4adcad0cb974aaca/coverage-7.6.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9901d36492009a0a9b94b20e52ebfc8453bf49bb2b27bca2c9706f8b4f5a554a", size = 207563 }, + { url = "https://files.pythonhosted.org/packages/a7/07/0bc73da0ccaf45d0d64ef86d33b7d7fdeef84b4c44bf6b85fb12c215c5a6/coverage-7.6.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abd3e72dd5b97e3af4246cdada7738ef0e608168de952b837b8dd7e90341f015", size = 240580 }, + { url = "https://files.pythonhosted.org/packages/71/8a/9761f409910961647d892454687cedbaccb99aae828f49486734a82ede6e/coverage-7.6.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff74026a461eb0660366fb01c650c1d00f833a086b336bdad7ab00cc952072b3", size = 237613 }, + { url = "https://files.pythonhosted.org/packages/8b/10/ee7d696a17ac94f32f2dbda1e17e730bf798ae9931aec1fc01c1944cd4de/coverage-7.6.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65dad5a248823a4996724a88eb51d4b31587aa7aa428562dbe459c684e5787ae", size = 239684 }, + { url = "https://files.pythonhosted.org/packages/16/60/aa1066040d3c52fff051243c2d6ccda264da72dc6d199d047624d395b2b2/coverage-7.6.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22be16571504c9ccea919fcedb459d5ab20d41172056206eb2994e2ff06118a4", size = 239112 }, + { url = "https://files.pythonhosted.org/packages/4e/e5/69f35344c6f932ba9028bf168d14a79fedb0dd4849b796d43c81ce75a3c9/coverage-7.6.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f957943bc718b87144ecaee70762bc2bc3f1a7a53c7b861103546d3a403f0a6", size = 237428 }, + { url = "https://files.pythonhosted.org/packages/32/20/adc895523c4a28f63441b8ac645abd74f9bdd499d2d175bef5b41fc7f92d/coverage-7.6.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ae1387db4aecb1f485fb70a6c0148c6cdaebb6038f1d40089b1fc84a5db556f", size = 239098 }, + { url = "https://files.pythonhosted.org/packages/a9/a6/e0e74230c9bb3549ec8ffc137cfd16ea5d56e993d6bffed2218bff6187e3/coverage-7.6.9-cp312-cp312-win32.whl", hash = "sha256:1a330812d9cc7ac2182586f6d41b4d0fadf9be9049f350e0efb275c8ee8eb692", size = 209940 }, + { url = "https://files.pythonhosted.org/packages/3e/18/cb5b88349d4aa2f41ec78d65f92ea32572b30b3f55bc2b70e87578b8f434/coverage-7.6.9-cp312-cp312-win_amd64.whl", hash = "sha256:b12c6b18269ca471eedd41c1b6a1065b2f7827508edb9a7ed5555e9a56dcfc97", size = 210726 }, + { url = "https://files.pythonhosted.org/packages/35/26/9abab6539d2191dbda2ce8c97b67d74cbfc966cc5b25abb880ffc7c459bc/coverage-7.6.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:899b8cd4781c400454f2f64f7776a5d87bbd7b3e7f7bda0cb18f857bb1334664", size = 207356 }, + { url = "https://files.pythonhosted.org/packages/44/da/d49f19402240c93453f606e660a6676a2a1fbbaa6870cc23207790aa9697/coverage-7.6.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61f70dc68bd36810972e55bbbe83674ea073dd1dcc121040a08cdf3416c5349c", size = 207614 }, + { url = "https://files.pythonhosted.org/packages/da/e6/93bb9bf85497816082ec8da6124c25efa2052bd4c887dd3b317b91990c9e/coverage-7.6.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a289d23d4c46f1a82d5db4abeb40b9b5be91731ee19a379d15790e53031c014", size = 240129 }, + { url = "https://files.pythonhosted.org/packages/df/65/6a824b9406fe066835c1274a9949e06f084d3e605eb1a602727a27ec2fe3/coverage-7.6.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e216d8044a356fc0337c7a2a0536d6de07888d7bcda76febcb8adc50bdbbd00", size = 237276 }, + { url = "https://files.pythonhosted.org/packages/9f/79/6c7a800913a9dd23ac8c8da133ebb556771a5a3d4df36b46767b1baffd35/coverage-7.6.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c026eb44f744acaa2bda7493dad903aa5bf5fc4f2554293a798d5606710055d", size = 239267 }, + { url = "https://files.pythonhosted.org/packages/57/e7/834d530293fdc8a63ba8ff70033d5182022e569eceb9aec7fc716b678a39/coverage-7.6.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e77363e8425325384f9d49272c54045bbed2f478e9dd698dbc65dbc37860eb0a", size = 238887 }, + { url = "https://files.pythonhosted.org/packages/15/05/ec9d6080852984f7163c96984444e7cd98b338fd045b191064f943ee1c08/coverage-7.6.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:777abfab476cf83b5177b84d7486497e034eb9eaea0d746ce0c1268c71652077", size = 236970 }, + { url = "https://files.pythonhosted.org/packages/0a/d8/775937670b93156aec29f694ce37f56214ed7597e1a75b4083ee4c32121c/coverage-7.6.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:447af20e25fdbe16f26e84eb714ba21d98868705cb138252d28bc400381f6ffb", size = 238831 }, + { url = "https://files.pythonhosted.org/packages/f4/58/88551cb7fdd5ec98cb6044e8814e38583436b14040a5ece15349c44c8f7c/coverage-7.6.9-cp313-cp313-win32.whl", hash = "sha256:d872ec5aeb086cbea771c573600d47944eea2dcba8be5f3ee649bfe3cb8dc9ba", size = 210000 }, + { url = "https://files.pythonhosted.org/packages/b7/12/cfbf49b95120872785ff8d56ab1c7fe3970a65e35010c311d7dd35c5fd00/coverage-7.6.9-cp313-cp313-win_amd64.whl", hash = "sha256:fd1213c86e48dfdc5a0cc676551db467495a95a662d2396ecd58e719191446e1", size = 210753 }, + { url = "https://files.pythonhosted.org/packages/7c/68/c1cb31445599b04bde21cbbaa6d21b47c5823cdfef99eae470dfce49c35a/coverage-7.6.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9e7484d286cd5a43744e5f47b0b3fb457865baf07bafc6bee91896364e1419", size = 208091 }, + { url = "https://files.pythonhosted.org/packages/11/73/84b02c6b19c4a11eb2d5b5eabe926fb26c21c080e0852f5e5a4f01165f9e/coverage-7.6.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1cf0872ee455c03e5674b5bca5e3e68e159379c1af0903e89f5eba9ccc3a", size = 208369 }, + { url = "https://files.pythonhosted.org/packages/de/e0/ae5d878b72ff26df2e994a5c5b1c1f6a7507d976b23beecb1ed4c85411ef/coverage-7.6.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d10e07aa2b91835d6abec555ec8b2733347956991901eea6ffac295f83a30e4", size = 251089 }, + { url = "https://files.pythonhosted.org/packages/ab/9c/0aaac011aef95a93ef3cb2fba3fde30bc7e68a6635199ed469b1f5ea355a/coverage-7.6.9-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13a9e2d3ee855db3dd6ea1ba5203316a1b1fd8eaeffc37c5b54987e61e4194ae", size = 246806 }, + { url = "https://files.pythonhosted.org/packages/f8/19/4d5d3ae66938a7dcb2f58cef3fa5386f838f469575b0bb568c8cc9e3a33d/coverage-7.6.9-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c38bf15a40ccf5619fa2fe8f26106c7e8e080d7760aeccb3722664c8656b030", size = 249164 }, + { url = "https://files.pythonhosted.org/packages/b3/0b/4ee8a7821f682af9ad440ae3c1e379da89a998883271f088102d7ca2473d/coverage-7.6.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d5275455b3e4627c8e7154feaf7ee0743c2e7af82f6e3b561967b1cca755a0be", size = 248642 }, + { url = "https://files.pythonhosted.org/packages/8a/12/36ff1d52be18a16b4700f561852e7afd8df56363a5edcfb04cf26a0e19e0/coverage-7.6.9-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8f8770dfc6e2c6a2d4569f411015c8d751c980d17a14b0530da2d7f27ffdd88e", size = 246516 }, + { url = "https://files.pythonhosted.org/packages/43/d0/8e258f6c3a527c1655602f4f576215e055ac704de2d101710a71a2affac2/coverage-7.6.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8d2dfa71665a29b153a9681edb1c8d9c1ea50dfc2375fb4dac99ea7e21a0bcd9", size = 247783 }, + { url = "https://files.pythonhosted.org/packages/a9/0d/1e4a48d289429d38aae3babdfcadbf35ca36bdcf3efc8f09b550a845bdb5/coverage-7.6.9-cp313-cp313t-win32.whl", hash = "sha256:5e6b86b5847a016d0fbd31ffe1001b63355ed309651851295315031ea7eb5a9b", size = 210646 }, + { url = "https://files.pythonhosted.org/packages/26/74/b0729f196f328ac55e42b1e22ec2f16d8bcafe4b8158a26ec9f1cdd1d93e/coverage-7.6.9-cp313-cp313t-win_amd64.whl", hash = "sha256:97ddc94d46088304772d21b060041c97fc16bdda13c6c7f9d8fcd8d5ae0d8611", size = 211815 }, + { url = "https://files.pythonhosted.org/packages/15/0e/4ac9035ee2ee08d2b703fdad2d84283ec0bad3b46eb4ad6affb150174cb6/coverage-7.6.9-pp39.pp310-none-any.whl", hash = "sha256:f3ca78518bc6bc92828cd11867b121891d75cae4ea9e908d72030609b996db1b", size = 199270 }, +] + +[[package]] +name = "cssbeautifier" +version = "1.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "editorconfig" }, + { name = "jsbeautifier" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/66/9bfd2d69fb4479d38439076132a620972939f7949015563dce5e61d29a8b/cssbeautifier-1.15.1.tar.gz", hash = "sha256:9f7064362aedd559c55eeecf6b6bed65e05f33488dcbe39044f0403c26e1c006", size = 25673 } + +[[package]] +name = "decorator" +version = "5.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/0c/8d907af351aa16b42caae42f9d6aa37b900c67308052d10fdce809f8d952/decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330", size = 35016 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/50/83c593b07763e1161326b3b8c6686f0f4b0f24d5526546bee538c89837d6/decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186", size = 9073 }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604 }, +] + +[[package]] +name = "django" +version = "4.2.17" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "sqlparse" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/58/709978ddf7e9393c0a89b57a5edbd764ee76eeea68697af3f77f3820980b/Django-4.2.17.tar.gz", hash = "sha256:6b56d834cc94c8b21a8f4e775064896be3b4a4ca387f2612d4406a5927cd2fdc", size = 10437674 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/85/457360cb3de496382e35db4c2af054066df5c40e26df31400d0109a0500c/Django-4.2.17-py3-none-any.whl", hash = "sha256:3a93350214ba25f178d4045c0786c61573e7dbfa3c509b3551374f1e11ba8de0", size = 7993390 }, +] + +[[package]] +name = "django-appconf" +version = "1.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/e0/704b6453f21fac22f0ab128150e9782b7d38bc1ed09710ac2197ddc1751f/django-appconf-1.0.6.tar.gz", hash = "sha256:cfe87ea827c4ee04b9a70fab90b86d704cb02f2981f89da8423cb0fabf88efbf", size = 15895 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/98/1cb3d9e8b1c6d0a74539b998474796fc5c0c0888b6201e5c95ba2f7a0677/django_appconf-1.0.6-py3-none-any.whl", hash = "sha256:c3ae442fba1ff7ec830412c5184b17169a7a1e71cf0864a4c3f93cf4c98a1993", size = 6424 }, +] + +[[package]] +name = "django-bootstrap-icons" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "defusedxml" }, + { name = "django" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/a3/0c94479fc8b6ef7eaf2c3e8d479e038d99d06c2884c6c21efec5c1d3e22e/django_bootstrap_icons-0.9.0.tar.gz", hash = "sha256:6856d3733a4049f5bd046301148dd15e4d291a5d1060a2f9533ef237f96a6bea", size = 11369 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/bc/7a755e8c6152e38e6e7afb3d707cf2ad521e873b9c860b3feb8dc927e560/django_bootstrap_icons-0.9.0-py3-none-any.whl", hash = "sha256:79aa88db04bc46ecc4420514dfaa026481d8e2f94fe963a543a4f26b1f16161a", size = 8256 }, +] + +[[package]] +name = "django-commoncontent" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "django-bootstrap-icons" }, + { name = "django-imagekit" }, + { name = "django-sitevars" }, + { name = "django-taggit" }, + { name = "mistletoe" }, + { name = "pillow" }, + { name = "pygments" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/9c/296070378378d47036e1ee5b085453a44dad92f168c151c3a9c7940e5111/django_commoncontent-0.3.0.tar.gz", hash = "sha256:761f2096ea39632b01ca51012ca5acd8f218eb1a8f6b57a734771cf928e8b0f0", size = 117759 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/e9/29465a5479a362611992f14ef295c97426d5fdb9e7105b3ee7db545aa33b/django_commoncontent-0.3.0-py3-none-any.whl", hash = "sha256:2e680e538d502aa8a9adf8a52c900f98da4a108bb8029f1818abf0916ebae431", size = 56086 }, +] + +[[package]] +name = "django-debug-toolbar" +version = "4.4.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "sqlparse" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/9c/0a3238eda0a46df20f2e3fe2a30313d34f5042a1a737d08230b77c29a3e9/django_debug_toolbar-4.4.6.tar.gz", hash = "sha256:36e421cb908c2f0675e07f9f41e3d1d8618dc386392ec82d23bcfcd5d29c7044", size = 272610 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/33/2036a472eedfbe49240dffea965242b3f444de4ea4fbeceb82ccea33a2ce/django_debug_toolbar-4.4.6-py3-none-any.whl", hash = "sha256:3beb671c9ec44ffb817fad2780667f172bd1c067dbcabad6268ce39a81335f45", size = 229621 }, +] + +[[package]] +name = "django-environ" +version = "0.11.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/0b/f2c024529ee4bbf8b95176eebeb86c6e695192a9ce0e91059cb83a33c1d3/django-environ-0.11.2.tar.gz", hash = "sha256:f32a87aa0899894c27d4e1776fa6b477e8164ed7f6b3e410a62a6d72caaf64be", size = 54326 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/f1/468b49cccba3b42dda571063a14c668bb0b53a1d5712426d18e36663bd53/django_environ-0.11.2-py2.py3-none-any.whl", hash = "sha256:0ff95ab4344bfeff693836aa978e6840abef2e2f1145adff7735892711590c05", size = 19141 }, +] + +[[package]] +name = "django-extensions" +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/f1/318684c9466968bf9a9c221663128206e460c1a67f595055be4b284cde8a/django-extensions-3.2.3.tar.gz", hash = "sha256:44d27919d04e23b3f40231c4ab7af4e61ce832ef46d610cc650d53e68328410a", size = 277216 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/7e/ba12b9660642663f5273141018d2bec0a1cae1711f4f6d1093920e157946/django_extensions-3.2.3-py3-none-any.whl", hash = "sha256:9600b7562f79a92cbf1fde6403c04fee314608fefbb595502e34383ae8203401", size = 229868 }, +] + +[[package]] +name = "django-imagekit" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django-appconf" }, + { name = "pilkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/cd/02611fc0cda31b17fec62c3235234ccf9920a4137de2b677e603749b968e/django-imagekit-5.0.0.tar.gz", hash = "sha256:aae9f74a8e9b6ceb5d15f7d8e266302901e76d9f532c78bd5135cb0fa206a6b0", size = 60041 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/0e/a2a2044e7dab7c7aced92574b7428d65edc370024b94a72d5f00293076fe/django_imagekit-5.0.0-py3-none-any.whl", hash = "sha256:a8e77ed6549751026a51f961bb2cd5fda739be691496da8eecbe68ffb966c261", size = 39240 }, +] + +[[package]] +name = "django-rich" +version = "1.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/f0/d3c450be9b319f9aba590decaf2858479e2399e78153c66045965bc9b951/django_rich-1.13.0.tar.gz", hash = "sha256:2a59a81ca3ba25202db7b6047aa322a8d5b866e1929f7fbea6059a3c8e4e420c", size = 61060 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/67/a81ebabc4c609045d884fc997087023c66a6b384d43bc0f039d17bc47fc3/django_rich-1.13.0-py3-none-any.whl", hash = "sha256:9146dc0e0134bb970f156e8ba3015abd8982c35418fbab45004c561b521ee509", size = 8855 }, +] + +[[package]] +name = "django-sitevars" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/5e/4fcf9c75be9a1e372b210de2483d1d67b37492f6574647cefd203d50b79d/django_sitevars-1.0.2.tar.gz", hash = "sha256:c92c3ed2fa45f4ade96e3c70d1130368883cd63e36689de60b99fab1d88055bc", size = 19390 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/36/d7b0f979d7f1196122ef668abb27668d4dc7916b6e4ce07422157107d654/django_sitevars-1.0.2-py3-none-any.whl", hash = "sha256:71ac33cbeec76a29a30a207b5c3e7b1476067acfcc2f2f0d7b03ac62a768c817", size = 12557 }, +] + +[[package]] +name = "django-taggit" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/a6/f1beaf8f552fe90c153cc039316ebab942c23dfbc88588dde081fefca816/django_taggit-6.1.0.tar.gz", hash = "sha256:c4d1199e6df34125dd36db5eb0efe545b254dec3980ce5dd80e6bab3e78757c3", size = 38151 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/34/4185c345530b91d05cb82e05d07148f481a5eb5dc2ac44e092b3daa6f206/django_taggit-6.1.0-py3-none-any.whl", hash = "sha256:ab776264bbc76cb3d7e49e1bf9054962457831bd21c3a42db9138b41956e4cf0", size = 75749 }, +] + +[[package]] +name = "django-tinymce" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/36/56faa2cb1bde28c66ffe38d67ddbacab9c416baecbb7dd911bfabc5fa409/django_tinymce-4.1.0.tar.gz", hash = "sha256:02e3b70e940fd299f0fbef4315aee5c185664e1eb8cd396b176963954e4357c9", size = 1087250 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/a6/467464d08495360689380906db8e67427c381545ce8af9ce633e0128b004/django_tinymce-4.1.0-py3-none-any.whl", hash = "sha256:9804836e6d2b08de3b03a27c100f8c2e9633549913eff8b323678a10cd48b94e", size = 1317923 }, +] + +[[package]] +name = "djlint" +version = "1.36.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama" }, + { name = "cssbeautifier" }, + { name = "jsbeautifier" }, + { name = "json5" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/dd/a442814263f6ef16153f87fe78658097c6690106f53f0d2594bf4ee81202/djlint-1.36.3.tar.gz", hash = "sha256:d85735da34bc7ac93ad8ef9b4822cc2a23d5f0ce33f25438737b8dca1d404f78", size = 47657 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/aa/a5128e91bfed1092bde177c34557bc04eea8043668b4fe06cdb5c47d54e8/djlint-1.36.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2ae7c620b58e16d6bf003bd7de3f71376a7a3daa79dc02e77f3726d5a75243f2", size = 352722 }, + { url = "https://files.pythonhosted.org/packages/60/e8/96ab0aacc02c017e31d28dccfd0e5ae83f008cfee6ae475406665c750b49/djlint-1.36.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e155ce0970d4a28d0a2e9f2e106733a2ad05910eee90e056b056d48049e4a97b", size = 327180 }, + { url = "https://files.pythonhosted.org/packages/ce/3d/d4284f37c05b537f5c5dfd8f57622248a1311c321571e6b30633e0b304c6/djlint-1.36.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e8bb0406e60cc696806aa6226df137618f3889c72f2dbdfa76c908c99151579", size = 413631 }, + { url = "https://files.pythonhosted.org/packages/05/f1/43874a4d866028ae2fce3c3ad55f169c50b82b43981127f87029300d1423/djlint-1.36.3-cp310-cp310-win_amd64.whl", hash = "sha256:76d32faf988ad58ef2e7a11d04046fc984b98391761bf1b61f9a6044da53d414", size = 359915 }, + { url = "https://files.pythonhosted.org/packages/74/b3/069e7be7d58be959bf5fde761acfa67465b9fc10ddbbaedea8b318ee01db/djlint-1.36.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:32f7a5834000fff22e94d1d35f95aaf2e06f2af2cae18af0ed2a4e215d60e730", size = 343501 }, + { url = "https://files.pythonhosted.org/packages/68/32/92481502ce125b8578b1625ed85e423ee1493abd35ac5df69e2cab3c20d2/djlint-1.36.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3eb1b9c0be499e63e8822a051e7e55f188ff1ab8172a85d338a8ae21c872060e", size = 318094 }, + { url = "https://files.pythonhosted.org/packages/18/c4/f7cc9b494f6ffbfc180218e7c7e5af303589350c5c4410a9600fb8e256fc/djlint-1.36.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c2e0dd1f26eb472b8c84eb70d6482877b6497a1fd031d7534864088f016d5ea", size = 404689 }, + { url = "https://files.pythonhosted.org/packages/f2/f9/88f04ae1d3323a268c8823868c55e6065be6a37cda028b0eb8d113fdf18a/djlint-1.36.3-cp311-cp311-win_amd64.whl", hash = "sha256:a06b531ab9d049c46ad4d2365d1857004a1a9dd0c23c8eae94aa0d233c6ec00d", size = 359580 }, + { url = "https://files.pythonhosted.org/packages/64/c7/721f9882300d477e6bf3d0e26b935bd38e3be6f152e81ac52b025575a15d/djlint-1.36.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e66361a865e5e5a4bbcb40f56af7f256fd02cbf9d48b763a40172749cc294084", size = 354201 }, + { url = "https://files.pythonhosted.org/packages/68/e8/282228db12c2d0236afab8dd11ffc1cf0e22355d03c8144c69d41979ba38/djlint-1.36.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:36e102b80d83e9ac2e6be9a9ded32fb925945f6dbc7a7156e4415de1b0aa0dba", size = 322234 }, + { url = "https://files.pythonhosted.org/packages/a8/fb/d6709ee3bcb66f1d9876b518c7a099366db06631212b2de24856f1341ecb/djlint-1.36.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ac4b7370d80bd82281e57a470de8923ac494ffb571b89d8787cef57c738c69a", size = 409797 }, + { url = "https://files.pythonhosted.org/packages/d9/c0/7b473515c41135891523698cfc8e781213cde9b28ef5aeba0f22bd2d8368/djlint-1.36.3-cp312-cp312-win_amd64.whl", hash = "sha256:107cc56bbef13d60cc0ae774a4d52881bf98e37c02412e573827a3e549217e3a", size = 361005 }, + { url = "https://files.pythonhosted.org/packages/3b/42/309c2ab239bc228771825ff75867ab25ed60f17f2f9f579c1dbb2aa93c28/djlint-1.36.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2a9f51971d6e63c41ea9b3831c928e1f21ae6fe57e87a3452cfe672d10232433", size = 353831 }, + { url = "https://files.pythonhosted.org/packages/de/05/b8cd7e4b2a15457324bc86c39458e606c00a30b3869c2e19e6a2890808b1/djlint-1.36.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:080c98714b55d8f0fef5c42beaee8247ebb2e3d46b0936473bd6c47808bb6302", size = 322102 }, + { url = "https://files.pythonhosted.org/packages/5e/24/bd818c6e6f343bd810ea9268e1f9cdb7464daabdc02926d61404e4a43b90/djlint-1.36.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f65a80e0b5cb13d357ea51ca6570b34c2d9d18974c1e57142de760ea27d49ed0", size = 409523 }, + { url = "https://files.pythonhosted.org/packages/6f/14/ddb0641f32a58c3e652d22c29fad4fe7eb29c994ada3801836cb82ff0fa3/djlint-1.36.3-cp313-cp313-win_amd64.whl", hash = "sha256:95ef6b67ef7f2b90d9434bba37d572031079001dc8524add85c00ef0386bda1e", size = 361052 }, + { url = "https://files.pythonhosted.org/packages/cb/d7/a909c3a5600560ecffe2110749b413672a2f41e7138a0e8a4f020054dbd0/djlint-1.36.3-py3-none-any.whl", hash = "sha256:0c05cd5b76785de2c41a2420c06ffd112800bfc0f9c0f399cc7cea7c42557f4c", size = 52166 }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 }, +] + +[[package]] +name = "editorconfig" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/29/785595a0d8b30ab8d2486559cfba1d46487b8dcbd99f74960b6b4cca92a4/editorconfig-0.17.0.tar.gz", hash = "sha256:8739052279699840065d3a9f5c125d7d5a98daeefe53b0e5274261d77cb49aa2", size = 13369 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/e5/8dba39ea24ca3de0e954e668107692f4dfc13a85300a531fa9a39e83fde4/EditorConfig-0.17.0-py3-none-any.whl", hash = "sha256:fe491719c5f65959ec00b167d07740e7ffec9a3f362038c72b289330b9991dfc", size = 16276 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[package]] +name = "executing" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/e3/7d45f492c2c4a0e8e0fad57d081a7c8a0286cdd86372b070cca1ec0caa1e/executing-2.1.0.tar.gz", hash = "sha256:8ea27ddd260da8150fa5a708269c4a10e76161e2496ec3e587da9e3c0fe4b9ab", size = 977485 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/fd/afcd0496feca3276f509df3dbd5dae726fcc756f1a08d9e25abe1733f962/executing-2.1.0-py2.py3-none-any.whl", hash = "sha256:8d63781349375b5ebccc3142f4b30350c0cd9c79f921cde38be2be4637e98eaf", size = 25805 }, +] + +[[package]] +name = "gunicorn" +version = "23.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "ipython" +version = "8.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/8b/710af065ab8ed05649afa5bd1e07401637c9ec9fb7cfda9eac7e91e9fbd4/ipython-8.30.0.tar.gz", hash = "sha256:cb0a405a306d2995a5cbb9901894d240784a9f341394c6ba3f4fe8c6eb89ff6e", size = 5592205 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/f3/1332ba2f682b07b304ad34cad2f003adcfeb349486103f4b632335074a7c/ipython-8.30.0-py3-none-any.whl", hash = "sha256:85ec56a7e20f6c38fce7727dcca699ae4ffc85985aa7b23635a8008f918ae321", size = 820765 }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278 }, +] + +[[package]] +name = "jsbeautifier" +version = "1.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "editorconfig" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/3e/dd37e1a7223247e3ef94714abf572415b89c4e121c4af48e9e4c392e2ca0/jsbeautifier-1.15.1.tar.gz", hash = "sha256:ebd733b560704c602d744eafc839db60a1ee9326e30a2a80c4adb8718adc1b24", size = 75606 } + +[[package]] +name = "json5" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/3d/bbe62f3d0c05a689c711cff57b2e3ac3d3e526380adb7c781989f075115c/json5-0.10.0.tar.gz", hash = "sha256:e66941c8f0a02026943c52c2eb34ebeb2a6f819a0be05920a6f5243cd30fd559", size = 48202 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/42/797895b952b682c3dafe23b1834507ee7f02f4d6299b65aaa61425763278/json5-0.10.0-py3-none-any.whl", hash = "sha256:19b23410220a7271e8377f81ba8aacba2fdd56947fbb137ee5977cbe1f5e8dfa", size = 34049 }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899 }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "mistletoe" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/96/ea46a376a7c4cd56955ecdfff0ea68de43996a4e6d1aee4599729453bd11/mistletoe-1.4.0.tar.gz", hash = "sha256:1630f906e5e4bbe66fdeb4d29d277e2ea515d642bb18a9b49b136361a9818c9d", size = 107203 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/0f/b5e545f0c7962be90366af3418989b12cf441d9da1e5d89d88f2f3e5cf8f/mistletoe-1.4.0-py3-none-any.whl", hash = "sha256:44a477803861de1237ba22e375c6b617690a31d2902b47279d1f8f7ed498a794", size = 51304 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "parso" +version = "0.8.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650 }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 }, +] + +[[package]] +name = "pilkit" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/a5/bbe12d2c9dc06e29224c45a2cd7aa0ce923648588b6a15aadfee150bbd0c/pilkit-3.0.tar.gz", hash = "sha256:f6719e8cc0482e5447f5cb94f18b949d8e604ea9673a9b019c74d41b779e4eab", size = 402342 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/ec/877b84b82cbcba6203e39d068cecfdbcfb61de69260cb851ea11be9b67f3/pilkit-3.0-py3-none-any.whl", hash = "sha256:fe1707b0411a1d0cbf9ad3986779fa5a346cec4582a188740924aa39f504d117", size = 20073 }, +] + +[[package]] +name = "pillow" +version = "11.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/26/0d95c04c868f6bdb0c447e3ee2de5564411845e36a858cfd63766bc7b563/pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739", size = 46737780 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/fb/a6ce6836bd7fd93fbf9144bf54789e02babc27403b50a9e1583ee877d6da/pillow-11.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947", size = 3154708 }, + { url = "https://files.pythonhosted.org/packages/6a/1d/1f51e6e912d8ff316bb3935a8cda617c801783e0b998bf7a894e91d3bd4c/pillow-11.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba", size = 2979223 }, + { url = "https://files.pythonhosted.org/packages/90/83/e2077b0192ca8a9ef794dbb74700c7e48384706467067976c2a95a0f40a1/pillow-11.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086", size = 4183167 }, + { url = "https://files.pythonhosted.org/packages/0e/74/467af0146970a98349cdf39e9b79a6cc8a2e7558f2c01c28a7b6b85c5bda/pillow-11.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9", size = 4283912 }, + { url = "https://files.pythonhosted.org/packages/85/b1/d95d4f7ca3a6c1ae120959605875a31a3c209c4e50f0029dc1a87566cf46/pillow-11.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488", size = 4195815 }, + { url = "https://files.pythonhosted.org/packages/41/c3/94f33af0762ed76b5a237c5797e088aa57f2b7fa8ee7932d399087be66a8/pillow-11.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f", size = 4366117 }, + { url = "https://files.pythonhosted.org/packages/ba/3c/443e7ef01f597497268899e1cca95c0de947c9bbf77a8f18b3c126681e5d/pillow-11.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb", size = 4278607 }, + { url = "https://files.pythonhosted.org/packages/26/95/1495304448b0081e60c0c5d63f928ef48bb290acee7385804426fa395a21/pillow-11.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97", size = 4410685 }, + { url = "https://files.pythonhosted.org/packages/45/da/861e1df971ef0de9870720cb309ca4d553b26a9483ec9be3a7bf1de4a095/pillow-11.0.0-cp310-cp310-win32.whl", hash = "sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50", size = 2249185 }, + { url = "https://files.pythonhosted.org/packages/d5/4e/78f7c5202ea2a772a5ab05069c1b82503e6353cd79c7e474d4945f4b82c3/pillow-11.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c", size = 2566726 }, + { url = "https://files.pythonhosted.org/packages/77/e4/6e84eada35cbcc646fc1870f72ccfd4afacb0fae0c37ffbffe7f5dc24bf1/pillow-11.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1", size = 2254585 }, + { url = "https://files.pythonhosted.org/packages/f0/eb/f7e21b113dd48a9c97d364e0915b3988c6a0b6207652f5a92372871b7aa4/pillow-11.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc", size = 3154705 }, + { url = "https://files.pythonhosted.org/packages/25/b3/2b54a1d541accebe6bd8b1358b34ceb2c509f51cb7dcda8687362490da5b/pillow-11.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a", size = 2979222 }, + { url = "https://files.pythonhosted.org/packages/20/12/1a41eddad8265c5c19dda8fb6c269ce15ee25e0b9f8f26286e6202df6693/pillow-11.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3", size = 4190220 }, + { url = "https://files.pythonhosted.org/packages/a9/9b/8a8c4d07d77447b7457164b861d18f5a31ae6418ef5c07f6f878fa09039a/pillow-11.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5", size = 4291399 }, + { url = "https://files.pythonhosted.org/packages/fc/e4/130c5fab4a54d3991129800dd2801feeb4b118d7630148cd67f0e6269d4c/pillow-11.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b", size = 4202709 }, + { url = "https://files.pythonhosted.org/packages/39/63/b3fc299528d7df1f678b0666002b37affe6b8751225c3d9c12cf530e73ed/pillow-11.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa", size = 4372556 }, + { url = "https://files.pythonhosted.org/packages/c6/a6/694122c55b855b586c26c694937d36bb8d3b09c735ff41b2f315c6e66a10/pillow-11.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306", size = 4287187 }, + { url = "https://files.pythonhosted.org/packages/ba/a9/f9d763e2671a8acd53d29b1e284ca298bc10a595527f6be30233cdb9659d/pillow-11.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9", size = 4418468 }, + { url = "https://files.pythonhosted.org/packages/6e/0e/b5cbad2621377f11313a94aeb44ca55a9639adabcaaa073597a1925f8c26/pillow-11.0.0-cp311-cp311-win32.whl", hash = "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5", size = 2249249 }, + { url = "https://files.pythonhosted.org/packages/dc/83/1470c220a4ff06cd75fc609068f6605e567ea51df70557555c2ab6516b2c/pillow-11.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291", size = 2566769 }, + { url = "https://files.pythonhosted.org/packages/52/98/def78c3a23acee2bcdb2e52005fb2810ed54305602ec1bfcfab2bda6f49f/pillow-11.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9", size = 2254611 }, + { url = "https://files.pythonhosted.org/packages/1c/a3/26e606ff0b2daaf120543e537311fa3ae2eb6bf061490e4fea51771540be/pillow-11.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923", size = 3147642 }, + { url = "https://files.pythonhosted.org/packages/4f/d5/1caabedd8863526a6cfa44ee7a833bd97f945dc1d56824d6d76e11731939/pillow-11.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903", size = 2978999 }, + { url = "https://files.pythonhosted.org/packages/d9/ff/5a45000826a1aa1ac6874b3ec5a856474821a1b59d838c4f6ce2ee518fe9/pillow-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4", size = 4196794 }, + { url = "https://files.pythonhosted.org/packages/9d/21/84c9f287d17180f26263b5f5c8fb201de0f88b1afddf8a2597a5c9fe787f/pillow-11.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f", size = 4300762 }, + { url = "https://files.pythonhosted.org/packages/84/39/63fb87cd07cc541438b448b1fed467c4d687ad18aa786a7f8e67b255d1aa/pillow-11.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9", size = 4210468 }, + { url = "https://files.pythonhosted.org/packages/7f/42/6e0f2c2d5c60f499aa29be14f860dd4539de322cd8fb84ee01553493fb4d/pillow-11.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7", size = 4381824 }, + { url = "https://files.pythonhosted.org/packages/31/69/1ef0fb9d2f8d2d114db982b78ca4eeb9db9a29f7477821e160b8c1253f67/pillow-11.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6", size = 4296436 }, + { url = "https://files.pythonhosted.org/packages/44/ea/dad2818c675c44f6012289a7c4f46068c548768bc6c7f4e8c4ae5bbbc811/pillow-11.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc", size = 4429714 }, + { url = "https://files.pythonhosted.org/packages/af/3a/da80224a6eb15bba7a0dcb2346e2b686bb9bf98378c0b4353cd88e62b171/pillow-11.0.0-cp312-cp312-win32.whl", hash = "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6", size = 2249631 }, + { url = "https://files.pythonhosted.org/packages/57/97/73f756c338c1d86bb802ee88c3cab015ad7ce4b838f8a24f16b676b1ac7c/pillow-11.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47", size = 2567533 }, + { url = "https://files.pythonhosted.org/packages/0b/30/2b61876e2722374558b871dfbfcbe4e406626d63f4f6ed92e9c8e24cac37/pillow-11.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25", size = 2254890 }, + { url = "https://files.pythonhosted.org/packages/63/24/e2e15e392d00fcf4215907465d8ec2a2f23bcec1481a8ebe4ae760459995/pillow-11.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699", size = 3147300 }, + { url = "https://files.pythonhosted.org/packages/43/72/92ad4afaa2afc233dc44184adff289c2e77e8cd916b3ddb72ac69495bda3/pillow-11.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38", size = 2978742 }, + { url = "https://files.pythonhosted.org/packages/9e/da/c8d69c5bc85d72a8523fe862f05ababdc52c0a755cfe3d362656bb86552b/pillow-11.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2", size = 4194349 }, + { url = "https://files.pythonhosted.org/packages/cd/e8/686d0caeed6b998351d57796496a70185376ed9c8ec7d99e1d19ad591fc6/pillow-11.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2", size = 4298714 }, + { url = "https://files.pythonhosted.org/packages/ec/da/430015cec620d622f06854be67fd2f6721f52fc17fca8ac34b32e2d60739/pillow-11.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527", size = 4208514 }, + { url = "https://files.pythonhosted.org/packages/44/ae/7e4f6662a9b1cb5f92b9cc9cab8321c381ffbee309210940e57432a4063a/pillow-11.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa", size = 4380055 }, + { url = "https://files.pythonhosted.org/packages/74/d5/1a807779ac8a0eeed57f2b92a3c32ea1b696e6140c15bd42eaf908a261cd/pillow-11.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f", size = 4296751 }, + { url = "https://files.pythonhosted.org/packages/38/8c/5fa3385163ee7080bc13026d59656267daaaaf3c728c233d530e2c2757c8/pillow-11.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb", size = 4430378 }, + { url = "https://files.pythonhosted.org/packages/ca/1d/ad9c14811133977ff87035bf426875b93097fb50af747793f013979facdb/pillow-11.0.0-cp313-cp313-win32.whl", hash = "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798", size = 2249588 }, + { url = "https://files.pythonhosted.org/packages/fb/01/3755ba287dac715e6afdb333cb1f6d69740a7475220b4637b5ce3d78cec2/pillow-11.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de", size = 2567509 }, + { url = "https://files.pythonhosted.org/packages/c0/98/2c7d727079b6be1aba82d195767d35fcc2d32204c7a5820f822df5330152/pillow-11.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84", size = 2254791 }, + { url = "https://files.pythonhosted.org/packages/eb/38/998b04cc6f474e78b563716b20eecf42a2fa16a84589d23c8898e64b0ffd/pillow-11.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b", size = 3150854 }, + { url = "https://files.pythonhosted.org/packages/13/8e/be23a96292113c6cb26b2aa3c8b3681ec62b44ed5c2bd0b258bd59503d3c/pillow-11.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003", size = 2982369 }, + { url = "https://files.pythonhosted.org/packages/97/8a/3db4eaabb7a2ae8203cd3a332a005e4aba00067fc514aaaf3e9721be31f1/pillow-11.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2", size = 4333703 }, + { url = "https://files.pythonhosted.org/packages/28/ac/629ffc84ff67b9228fe87a97272ab125bbd4dc462745f35f192d37b822f1/pillow-11.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a", size = 4412550 }, + { url = "https://files.pythonhosted.org/packages/d6/07/a505921d36bb2df6868806eaf56ef58699c16c388e378b0dcdb6e5b2fb36/pillow-11.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8", size = 4461038 }, + { url = "https://files.pythonhosted.org/packages/d6/b9/fb620dd47fc7cc9678af8f8bd8c772034ca4977237049287e99dda360b66/pillow-11.0.0-cp313-cp313t-win32.whl", hash = "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8", size = 2253197 }, + { url = "https://files.pythonhosted.org/packages/df/86/25dde85c06c89d7fc5db17940f07aae0a56ac69aa9ccb5eb0f09798862a8/pillow-11.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904", size = 2572169 }, + { url = "https://files.pythonhosted.org/packages/51/85/9c33f2517add612e17f3381aee7c4072779130c634921a756c97bc29fb49/pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3", size = 2256828 }, + { url = "https://files.pythonhosted.org/packages/36/57/42a4dd825eab762ba9e690d696d894ba366e06791936056e26e099398cda/pillow-11.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2", size = 3119239 }, + { url = "https://files.pythonhosted.org/packages/98/f7/25f9f9e368226a1d6cf3507081a1a7944eddd3ca7821023377043f5a83c8/pillow-11.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2", size = 2950803 }, + { url = "https://files.pythonhosted.org/packages/59/01/98ead48a6c2e31e6185d4c16c978a67fe3ccb5da5c2ff2ba8475379bb693/pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b", size = 3281098 }, + { url = "https://files.pythonhosted.org/packages/51/c0/570255b2866a0e4d500a14f950803a2ec273bac7badc43320120b9262450/pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2", size = 3323665 }, + { url = "https://files.pythonhosted.org/packages/0e/75/689b4ec0483c42bfc7d1aacd32ade7a226db4f4fac57c6fdcdf90c0731e3/pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830", size = 3310533 }, + { url = "https://files.pythonhosted.org/packages/3d/30/38bd6149cf53da1db4bad304c543ade775d225961c4310f30425995cb9ec/pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734", size = 3414886 }, + { url = "https://files.pythonhosted.org/packages/ec/3d/c32a51d848401bd94cabb8767a39621496491ee7cd5199856b77da9b18ad/pillow-11.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316", size = 2567508 }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.48" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/4f/feb5e137aff82f7c7f3248267b97451da3644f6cdc218edfe549fb354127/prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90", size = 424684 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/6a/fd08d94654f7e67c52ca30523a178b3f8ccc4237fce4be90d39c938a831a/prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e", size = 386595 }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842 }, +] + +[[package]] +name = "pygments" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "regex" +version = "2024.11.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/3c/4651f6b130c6842a8f3df82461a8950f923925db8b6961063e82744bddcc/regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91", size = 482674 }, + { url = "https://files.pythonhosted.org/packages/15/51/9f35d12da8434b489c7b7bffc205c474a0a9432a889457026e9bc06a297a/regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0", size = 287684 }, + { url = "https://files.pythonhosted.org/packages/bd/18/b731f5510d1b8fb63c6b6d3484bfa9a59b84cc578ac8b5172970e05ae07c/regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e", size = 284589 }, + { url = "https://files.pythonhosted.org/packages/78/a2/6dd36e16341ab95e4c6073426561b9bfdeb1a9c9b63ab1b579c2e96cb105/regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde", size = 782511 }, + { url = "https://files.pythonhosted.org/packages/1b/2b/323e72d5d2fd8de0d9baa443e1ed70363ed7e7b2fb526f5950c5cb99c364/regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e", size = 821149 }, + { url = "https://files.pythonhosted.org/packages/90/30/63373b9ea468fbef8a907fd273e5c329b8c9535fee36fc8dba5fecac475d/regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2", size = 809707 }, + { url = "https://files.pythonhosted.org/packages/f2/98/26d3830875b53071f1f0ae6d547f1d98e964dd29ad35cbf94439120bb67a/regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf", size = 781702 }, + { url = "https://files.pythonhosted.org/packages/87/55/eb2a068334274db86208ab9d5599ffa63631b9f0f67ed70ea7c82a69bbc8/regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c", size = 771976 }, + { url = "https://files.pythonhosted.org/packages/74/c0/be707bcfe98254d8f9d2cff55d216e946f4ea48ad2fd8cf1428f8c5332ba/regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86", size = 697397 }, + { url = "https://files.pythonhosted.org/packages/49/dc/bb45572ceb49e0f6509f7596e4ba7031f6819ecb26bc7610979af5a77f45/regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67", size = 768726 }, + { url = "https://files.pythonhosted.org/packages/5a/db/f43fd75dc4c0c2d96d0881967897926942e935d700863666f3c844a72ce6/regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d", size = 775098 }, + { url = "https://files.pythonhosted.org/packages/99/d7/f94154db29ab5a89d69ff893159b19ada89e76b915c1293e98603d39838c/regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2", size = 839325 }, + { url = "https://files.pythonhosted.org/packages/f7/17/3cbfab1f23356fbbf07708220ab438a7efa1e0f34195bf857433f79f1788/regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008", size = 843277 }, + { url = "https://files.pythonhosted.org/packages/7e/f2/48b393b51900456155de3ad001900f94298965e1cad1c772b87f9cfea011/regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62", size = 773197 }, + { url = "https://files.pythonhosted.org/packages/45/3f/ef9589aba93e084cd3f8471fded352826dcae8489b650d0b9b27bc5bba8a/regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e", size = 261714 }, + { url = "https://files.pythonhosted.org/packages/42/7e/5f1b92c8468290c465fd50c5318da64319133231415a8aa6ea5ab995a815/regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519", size = 274042 }, + { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669 }, + { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684 }, + { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589 }, + { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121 }, + { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275 }, + { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257 }, + { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727 }, + { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667 }, + { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963 }, + { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700 }, + { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592 }, + { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929 }, + { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213 }, + { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734 }, + { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052 }, + { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781 }, + { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455 }, + { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759 }, + { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976 }, + { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077 }, + { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160 }, + { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896 }, + { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997 }, + { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725 }, + { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481 }, + { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896 }, + { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138 }, + { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692 }, + { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135 }, + { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567 }, + { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525 }, + { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324 }, + { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617 }, + { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023 }, + { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072 }, + { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130 }, + { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857 }, + { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006 }, + { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650 }, + { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545 }, + { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045 }, + { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182 }, + { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733 }, + { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122 }, + { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "rich" +version = "13.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, +] + +[[package]] +name = "setproctitle" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/4e/b09341b19b9ceb8b4c67298ab4a08ef7a4abdd3016c7bb152e9b6379031d/setproctitle-1.3.4.tar.gz", hash = "sha256:3b40d32a3e1f04e94231ed6dfee0da9e43b4f9c6b5450d53e6dd7754c34e0c50", size = 26456 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/f4/95937eb5c5370324a942ba90174c6d0fc7c5ad2f7f8ea989ccdbd6e1be5e/setproctitle-1.3.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0f6661a69c68349172ba7b4d5dd65fec2b0917abc99002425ad78c3e58cf7595", size = 16855 }, + { url = "https://files.pythonhosted.org/packages/32/a6/d49dbb0d75d02d11db49151469e1fee740efa45de7288bffcc4d88d0c290/setproctitle-1.3.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:754bac5e470adac7f7ec2239c485cd0b75f8197ca8a5b86ffb20eb3a3676cc42", size = 11627 }, + { url = "https://files.pythonhosted.org/packages/2e/cd/73a0fc913db50c3b736750ce67824f1108c2173e5d043a16ef9874b4f988/setproctitle-1.3.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7bc7088c15150745baf66db62a4ced4507d44419eb66207b609f91b64a682af", size = 31187 }, + { url = "https://files.pythonhosted.org/packages/63/0f/74f9112f7f506acc01f085811c6d135751b6fa42d30207f53b25579d043a/setproctitle-1.3.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a46ef3ecf61e4840fbc1145fdd38acf158d0da7543eda7b773ed2b30f75c2830", size = 32534 }, + { url = "https://files.pythonhosted.org/packages/3b/88/53eec2373745069d4c8a59d41ee2ef4a48949b77cccd0077c270261b238a/setproctitle-1.3.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffcb09d5c0ffa043254ec9a734a73f3791fec8bf6333592f906bb2e91ed2af1a", size = 29657 }, + { url = "https://files.pythonhosted.org/packages/50/1c/a4d3d8c20bf3bbafd8c5038e7da09043a9d21450b6a73694ada11c01b58a/setproctitle-1.3.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06c16b7a91cdc5d700271899e4383384a61aae83a3d53d0e2e5a266376083342", size = 30695 }, + { url = "https://files.pythonhosted.org/packages/a2/2a/9f290f0d10ea87a266d63025078eabfa040ad29ea10d815e167a5104de00/setproctitle-1.3.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9f9732e59863eaeedd3feef94b2b216cb86d40dda4fad2d0f0aaec3b31592716", size = 30340 }, + { url = "https://files.pythonhosted.org/packages/38/c4/5bfe02d4cdd16338973d452c7c6042abdd2827d90f7ce4e21bc003f2edb1/setproctitle-1.3.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e152f4ab9ea1632b5fecdd87cee354f2b2eb6e2dfc3aceb0eb36a01c1e12f94c", size = 29352 }, + { url = "https://files.pythonhosted.org/packages/b3/41/0dd85cef0e5a5a332bdda7b55738e950c2edecea3ae45c658990553d50f8/setproctitle-1.3.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:020ea47a79b2bbd7bd7b94b85ca956ba7cb026e82f41b20d2e1dac4008cead25", size = 31819 }, + { url = "https://files.pythonhosted.org/packages/d7/23/fbfacfed8805983a83324099484e47b9028d0d3c07a0fe017123eee3f580/setproctitle-1.3.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8c52b12b10e4057fc302bd09cb3e3f28bb382c30c044eb3396e805179a8260e4", size = 29745 }, + { url = "https://files.pythonhosted.org/packages/68/37/e18c5a00bfd1c4c2c815536d5c63a470e4364b571bd5096d38d0fe277bf5/setproctitle-1.3.4-cp310-cp310-win32.whl", hash = "sha256:a65a147f545f3fac86f11acb2d0b316d3e78139a9372317b7eb50561b2817ba0", size = 11358 }, + { url = "https://files.pythonhosted.org/packages/52/fd/1fae8c4c13af22d8d17816c44421085509a08dfa77f573d31447d6cd540c/setproctitle-1.3.4-cp310-cp310-win_amd64.whl", hash = "sha256:66821fada6426998762a3650a37fba77e814a249a95b1183011070744aff47f6", size = 12072 }, + { url = "https://files.pythonhosted.org/packages/5d/1a/1fb7d622195bcb3ce7b04366a833e51cfa5ad632c5dafe32e0763cd3fdc9/setproctitle-1.3.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f0f749f07002c2d6fecf37cedc43207a88e6c651926a470a5f229070cf791879", size = 16851 }, + { url = "https://files.pythonhosted.org/packages/46/54/e3aa4f46eddf795f10452ea878ff85c3496d36409636530f9a37e2de3cbe/setproctitle-1.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:90ea8d302a5d30b948451d146e94674a3c5b020cc0ced9a1c28f8ddb0f203a5d", size = 11620 }, + { url = "https://files.pythonhosted.org/packages/61/47/80988221679dfd93c464248abb71c2a96338f2ca3f8e3288d0ecb7422f4d/setproctitle-1.3.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f859c88193ed466bee4eb9d45fbc29d2253e6aa3ccd9119c9a1d8d95f409a60d", size = 31519 }, + { url = "https://files.pythonhosted.org/packages/2c/72/14984c127f708597e412f1a8cf7cac809b9bca50a267a6b01b221b094330/setproctitle-1.3.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3afa5a0ed08a477ded239c05db14c19af585975194a00adf594d48533b23701", size = 32860 }, + { url = "https://files.pythonhosted.org/packages/16/9d/34ea09295620fddae65cf7caeac81bbfc386a3ae6ce26a4dcadbb54c134d/setproctitle-1.3.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a78fce9018cc3e9a772b6537bbe3fe92380acf656c9f86db2f45e685af376e", size = 30029 }, + { url = "https://files.pythonhosted.org/packages/44/bf/a447a51054ceed23f69d4f7370289044b4508569f11da6db2eec087bc174/setproctitle-1.3.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d758e2eed2643afac5f2881542fbb5aa97640b54be20d0a5ed0691d02f0867d", size = 31017 }, + { url = "https://files.pythonhosted.org/packages/ec/46/adcffde6fb8d95458da0a568afdf0dabbbff6470299d94014676e1ab43c0/setproctitle-1.3.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ef133a1a2ee378d549048a12d56f4ef0e2b9113b0b25b6b77821e9af94d50634", size = 30762 }, + { url = "https://files.pythonhosted.org/packages/a3/cd/747a67ce1f6ef8fd1fa46b0b13ba0e007b80914bd549318830b8691ab9f6/setproctitle-1.3.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1d2a154b79d5fb42d1eff06e05e22f0e8091261d877dd47b37d31352b74ecc37", size = 29753 }, + { url = "https://files.pythonhosted.org/packages/3d/86/5939546e57238462a7839ae78399a635d1cfc5d125c7a12a28face111730/setproctitle-1.3.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:202eae632815571297833876a0f407d0d9c7ad9d843b38adbe687fe68c5192ee", size = 32161 }, + { url = "https://files.pythonhosted.org/packages/62/83/9194a4baed06e0e90a69e2e4a77a75e5a3ff008046870c79bc36a5c45e1c/setproctitle-1.3.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2b0080819859e80a7776ac47cf6accb4b7ad313baf55fabac89c000480dcd103", size = 30104 }, + { url = "https://files.pythonhosted.org/packages/ac/cd/08928fec23cbf4dae2a7b245b72d86e6458d64f4e7e6956cd80a9fda8c80/setproctitle-1.3.4-cp311-cp311-win32.whl", hash = "sha256:9c9d7d1267dee8c6627963d9376efa068858cfc8f573c083b1b6a2d297a8710f", size = 11349 }, + { url = "https://files.pythonhosted.org/packages/aa/19/240c4b99d57e045d3b2e2effa5924e810eabb18c56ef9c2336a7746dffe4/setproctitle-1.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:475986ddf6df65d619acd52188336a20f616589403f5a5ceb3fc70cdc137037a", size = 12071 }, + { url = "https://files.pythonhosted.org/packages/94/1f/02fb3c6038c819d86765316d2a911281fc56c7dd3a9355dceb3f26a5bf7b/setproctitle-1.3.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d06990dcfcd41bb3543c18dd25c8476fbfe1f236757f42fef560f6aa03ac8dfc", size = 16842 }, + { url = "https://files.pythonhosted.org/packages/b8/0c/d69e1f91c8f3d3aa74394e9e6ebb818f7d323e2d138ce1127e9462d09ebc/setproctitle-1.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:317218c9d8b17a010ab2d2f0851e8ef584077a38b1ba2b7c55c9e44e79a61e73", size = 11614 }, + { url = "https://files.pythonhosted.org/packages/86/ed/8031871d275302054b2f1b94b7cf5e850212cc412fe968f0979e64c1b838/setproctitle-1.3.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb5fefb53b9d9f334a5d9ec518a36b92a10b936011ac8a6b6dffd60135f16459", size = 31840 }, + { url = "https://files.pythonhosted.org/packages/45/b7/04f5d221cbdcff35d6cdf74e2a852e69dc8d8e746eb1b314be6b57b79c41/setproctitle-1.3.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0855006261635e8669646c7c304b494b6df0a194d2626683520103153ad63cc9", size = 33271 }, + { url = "https://files.pythonhosted.org/packages/25/b2/8dff0d2a72076e5535f117f33458d520538b5a0900b90a9f59a278f0d3f6/setproctitle-1.3.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a88e466fcaee659679c1d64dcb2eddbcb4bfadffeb68ba834d9c173a25b6184", size = 30509 }, + { url = "https://files.pythonhosted.org/packages/4b/cf/4f19cdc7fdff3eaeb3064ce6eeb27c63081dba3123fbf904ac6bf0de440c/setproctitle-1.3.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f963b6ed8ba33eda374a98d979e8a0eaf21f891b6e334701693a2c9510613c4c", size = 31543 }, + { url = "https://files.pythonhosted.org/packages/9b/a7/5f9c3c70dc5573f660f978fb3bb4847cd26ede95a5fc294d3f1cf6779800/setproctitle-1.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:122c2e05697fa91f5d23f00bbe98a9da1bd457b32529192e934095fadb0853f1", size = 31268 }, + { url = "https://files.pythonhosted.org/packages/26/ab/bbde90ea0ed6a062ef94fe1c609b68077f7eb586133a62fa62d0c8dd9f8c/setproctitle-1.3.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:1bba0a866f5895d5b769d8c36b161271c7fd407e5065862ab80ff91c29fbe554", size = 30232 }, + { url = "https://files.pythonhosted.org/packages/36/0e/817be9934eda4cf63c96c694c3383cb0d2e5d019a2871af7dbd2202f7a58/setproctitle-1.3.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:97f1f861998e326e640708488c442519ad69046374b2c3fe9bcc9869b387f23c", size = 32739 }, + { url = "https://files.pythonhosted.org/packages/b0/76/9b4877850c9c5f41c4bacae441285dead7c192bebf4fcbf3b3eb0e8033cc/setproctitle-1.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:726aee40357d4bdb70115442cb85ccc8e8bc554fc0bbbaa3a57cbe81df42287d", size = 30778 }, + { url = "https://files.pythonhosted.org/packages/b2/fa/bbc7ab32f253b9700ac20d78ba0d5fbdc4ea5789d33e1adb236cdf20b23a/setproctitle-1.3.4-cp312-cp312-win32.whl", hash = "sha256:04d6ba8b816dbb0bfd62000b0c3e583160893e6e8c4233e1dca1a9ae4d95d924", size = 11355 }, + { url = "https://files.pythonhosted.org/packages/44/5c/6e6665b5fd800206a9e537ab0d2630d7b9b31b4697d931ed468837cc9cf5/setproctitle-1.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:9c76e43cb351ba8887371240b599925cdf3ecececc5dfb7125c71678e7722c55", size = 12069 }, + { url = "https://files.pythonhosted.org/packages/d4/01/51d07ab1dbec8885ebad419d254c06b9e28f4363c163b737a89995a52b75/setproctitle-1.3.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d6e3b177e634aa6bbbfbf66d097b6d1cdb80fc60e912c7d8bace2e45699c07dd", size = 16831 }, + { url = "https://files.pythonhosted.org/packages/30/03/deff7089b525c0d8ec047e06661d2be67c87685a99be6a6aed2890b81c8f/setproctitle-1.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6b17655a5f245b416e127e02087ea6347a48821cc4626bc0fd57101bfcd88afc", size = 11607 }, + { url = "https://files.pythonhosted.org/packages/ea/be/cb2950b3f6ba460f530bda2c713828236c75d982d0aa0f62b33429a9b4d0/setproctitle-1.3.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa5057a86df920faab8ee83960b724bace01a3231eb8e3f2c93d78283504d598", size = 31881 }, + { url = "https://files.pythonhosted.org/packages/5c/b4/1f0dba7525a2fbefd08d4086e7e998d9c7581153807fb6b3083d06e0b8e2/setproctitle-1.3.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149fdfb8a26a555780c4ce53c92e6d3c990ef7b30f90a675eca02e83c6d5f76d", size = 33290 }, + { url = "https://files.pythonhosted.org/packages/2d/a8/07a160f9dcd1a7b1cad39ce6cbaf4425837502b0592a400c38cb21f0f247/setproctitle-1.3.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ded03546938a987f463c68ab98d683af87a83db7ac8093bbc179e77680be5ba2", size = 30489 }, + { url = "https://files.pythonhosted.org/packages/83/0c/3d972d9ea4165961a9764df5324d42bf2d059cb8a6ef516c67f068ed4d92/setproctitle-1.3.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ab9f5b7f2bbc1754bc6292d9a7312071058e5a891b0391e6d13b226133f36aa", size = 31576 }, + { url = "https://files.pythonhosted.org/packages/7a/c0/c12bdc2c91009defdd1b207ff156ccd691f5b9a6a0aae1ed9126d4ff9a0c/setproctitle-1.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0b19813c852566fa031902124336fa1f080c51e262fc90266a8c3d65ca47b74c", size = 31273 }, + { url = "https://files.pythonhosted.org/packages/4f/83/8d704bee57990b27537adf7c97540f32226ffa3922fb26bdd459da8a4470/setproctitle-1.3.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:db78b645dc63c0ccffca367a498f3b13492fb106a2243a1e998303ba79c996e2", size = 30236 }, + { url = "https://files.pythonhosted.org/packages/d8/42/94e31d1f515f831e1ae43f2405794257eb940a7972b2fbb6283790db2958/setproctitle-1.3.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b669aaac70bd9f03c070270b953f78d9ee56c4af6f0ff9f9cd3e6d1878c10b40", size = 32766 }, + { url = "https://files.pythonhosted.org/packages/83/53/01746ed8fb75239a001ee89d5eb8ad5a3022df510572d1cf60dd04567e13/setproctitle-1.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6dc3d656702791565994e64035a208be56b065675a5bc87b644c657d6d9e2232", size = 30812 }, + { url = "https://files.pythonhosted.org/packages/5f/ea/3ce61e70a6b898e95c0a1e393964c829103dc4ad4b0732cd70c8fc13e54c/setproctitle-1.3.4-cp313-cp313-win32.whl", hash = "sha256:091f682809a4d12291cf0205517619d2e7014986b7b00ebecfde3d76f8ae5a8f", size = 11349 }, + { url = "https://files.pythonhosted.org/packages/e7/1a/8149da1c19db6bd57164d62b1d91c188e7d77e695947cf1ac327c8aea513/setproctitle-1.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:adcd6ba863a315702184d92d3d3bbff290514f24a14695d310f02ae5e28bd1f7", size = 12062 }, + { url = "https://files.pythonhosted.org/packages/2f/d0/775418662081d44b91da236ed4503e10e7008c9c5fd30193e13db388fbef/setproctitle-1.3.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:939d364a187b2adfbf6ae488664277e717d56c7951a4ddeb4f23b281bc50bfe5", size = 11153 }, + { url = "https://files.pythonhosted.org/packages/fd/1f/b3b82633336cd9908bf74cbc06dd533025b3d3c202437c4e3d0bc871ca13/setproctitle-1.3.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb8a6a19be0cbf6da6fcbf3698b76c8af03fe83e4bd77c96c3922be3b88bf7da", size = 13310 }, + { url = "https://files.pythonhosted.org/packages/f5/89/887c6872ceed5ca344d25c8cc8a3f9b99bbcb25613c4b680476b29aabe23/setproctitle-1.3.4-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:779006f9e1aade9522a40e8d9635115ab15dd82b7af8e655967162e9c01e2573", size = 12911 }, + { url = "https://files.pythonhosted.org/packages/b0/8d/9e4a4651b1c5845a9aec0d2c08c65768ba5ca2ec76598124b45d384a5f46/setproctitle-1.3.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5519f2a7b8c535b0f1f77b30441476571373add72008230c81211ee17b423b57", size = 12105 }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "sqlparse" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", size = 84999 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415 }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521 }, +] + +[[package]] +name = "storyville" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "django" }, + { name = "django-commoncontent" }, + { name = "django-environ" }, + { name = "django-extensions" }, + { name = "django-rich" }, + { name = "django-sitevars" }, + { name = "django-tinymce" }, + { name = "docutils" }, + { name = "gunicorn" }, + { name = "pillow" }, + { name = "rich" }, + { name = "setproctitle" }, +] + +[package.dev-dependencies] +dev = [ + { name = "coverage" }, + { name = "django-debug-toolbar" }, + { name = "djlint" }, + { name = "ipython" }, +] + +[package.metadata] +requires-dist = [ + { name = "django", specifier = ">=4.2.7,<5.0" }, + { name = "django-commoncontent", specifier = ">=0.3.0" }, + { name = "django-environ", specifier = ">=0.10.0" }, + { name = "django-extensions", specifier = ">=3.2.1" }, + { name = "django-rich", specifier = ">=1.5.0" }, + { name = "django-sitevars", specifier = ">=1.0.2" }, + { name = "django-tinymce", specifier = ">=3.4.0" }, + { name = "docutils", specifier = ">=0.19" }, + { name = "gunicorn", specifier = ">=20.1.0" }, + { name = "pillow", specifier = ">=10.0.1" }, + { name = "rich", specifier = ">=13.3.3" }, + { name = "setproctitle", specifier = ">=1.3.2" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "coverage", specifier = ">=7.2.2" }, + { name = "django-debug-toolbar", specifier = ">=4.0.0" }, + { name = "djlint", specifier = ">=1.36.3" }, + { name = "ipython", specifier = ">=8.12" }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "tzdata" +version = "2024.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/34/943888654477a574a86a98e9896bae89c7aa15078ec29f490fef2f1e5384/tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", size = 193282 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/ab/7e5f53c3b9d14972843a647d8d7a853969a58aecc7559cb3267302c94774/tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd", size = 346586 }, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, +] From f9e6363cd832d73405d8cdc0f8cd19f79592f5ab Mon Sep 17 00:00:00 2001 From: Vince Veselosky Date: Thu, 19 Dec 2024 16:59:08 -0500 Subject: [PATCH 02/10] Switch to src layout --- manage.py | 173 +----------------- pyproject.toml | 6 +- {storyville => src/storyville}/__init__.py | 0 {storyville => src/storyville}/apps.py | 0 {storyville => src/storyville}/asgi.py | 0 {storyville => src/storyville}/celery.py | 0 .../storyville}/fixtures/smoketest.json | 0 {storyville => src/storyville}/settings.py | 13 +- .../Digital-Patreon-Logo_FieryCoral.png | Bin .../Digital-Patreon-Wordmark_FieryCoral.png | Bin .../static}/iqlovecraft/PatreonBanner-001.png | Bin .../static}/mindvessel.com/mindvessel.css | 0 .../static}/veselosky.me/CF-Banner.jpg | Bin .../cover-PC1-CursingFate-portrait_medium.jpg | Bin .../veselosky.me/cover-PC1-CursingFate.jpg | Bin ...-PC2-ShiftingLoyalties-portrait_medium.jpg | Bin .../cover-PC2-ShiftingLoyalties.jpg | Bin ...r-PC3-SummoningCourage-portrait_medium.jpg | Bin .../cover-PC3-SummoningCourage.jpg | Bin .../static}/veselosky.me/veselosky.css | 0 .../templates}/iqlovecraft/index.html | 0 .../storyville/blocks/header_masthead.html | 0 .../veselosky.me/blocks/novels_card.html | 0 .../templates}/veselosky.me/index.html | 0 {storyville => src/storyville}/urls.py | 0 {storyville => src/storyville}/wsgi.py | 0 26 files changed, 15 insertions(+), 177 deletions(-) rename {storyville => src/storyville}/__init__.py (100%) rename {storyville => src/storyville}/apps.py (100%) rename {storyville => src/storyville}/asgi.py (100%) rename {storyville => src/storyville}/celery.py (100%) rename {storyville => src/storyville}/fixtures/smoketest.json (100%) rename {storyville => src/storyville}/settings.py (96%) rename {static => src/storyville/static}/iqlovecraft/Digital-Patreon-Logo_FieryCoral.png (100%) rename {static => src/storyville/static}/iqlovecraft/Digital-Patreon-Wordmark_FieryCoral.png (100%) rename {static => src/storyville/static}/iqlovecraft/PatreonBanner-001.png (100%) rename {static => src/storyville/static}/mindvessel.com/mindvessel.css (100%) rename {static => src/storyville/static}/veselosky.me/CF-Banner.jpg (100%) rename {static => src/storyville/static}/veselosky.me/cover-PC1-CursingFate-portrait_medium.jpg (100%) rename {static => src/storyville/static}/veselosky.me/cover-PC1-CursingFate.jpg (100%) rename {static => src/storyville/static}/veselosky.me/cover-PC2-ShiftingLoyalties-portrait_medium.jpg (100%) rename {static => src/storyville/static}/veselosky.me/cover-PC2-ShiftingLoyalties.jpg (100%) rename {static => src/storyville/static}/veselosky.me/cover-PC3-SummoningCourage-portrait_medium.jpg (100%) rename {static => src/storyville/static}/veselosky.me/cover-PC3-SummoningCourage.jpg (100%) rename {static => src/storyville/static}/veselosky.me/veselosky.css (100%) rename {templates => src/storyville/templates}/iqlovecraft/index.html (100%) rename {templates => src/storyville/templates}/storyville/blocks/header_masthead.html (100%) rename {templates => src/storyville/templates}/veselosky.me/blocks/novels_card.html (100%) rename {templates => src/storyville/templates}/veselosky.me/index.html (100%) rename {storyville => src/storyville}/urls.py (100%) rename {storyville => src/storyville}/wsgi.py (100%) diff --git a/manage.py b/manage.py index b7ab5b4..833abfb 100755 --- a/manage.py +++ b/manage.py @@ -1,180 +1,17 @@ -#!/usr/bin/env python -"""Django's command-line utility for administrative tasks.""" - +#! /usr/bin/env python import os -import shutil -import subprocess import sys -import typing as T -import venv -from pathlib import Path - -PROJECT_DIR = Path(__file__).parent -dev_in = PROJECT_DIR / "requirements-dev.in" -base_in = PROJECT_DIR / "requirements.in" -dev_reqs = PROJECT_DIR / "requirements-dev.txt" -base_reqs = PROJECT_DIR / "requirements.txt" - - -def get_django_package() -> str: - """A simple hueristic to find the main package for this Django project.""" - for item in PROJECT_DIR.iterdir(): - if not item.is_dir(): - continue - if item.joinpath("settings.py").exists(): - # Found it - return item.name - - # If we fall through the loop, there is no settings.py, or this file is in the - # wrong place. - raise Exception( - "Unable to locate a Django settings file (searched */settings.py)." - "This script cannot continue without a settings file." - ) - - -def get_python_binary_from(VENV: Path) -> str: - for maybe in [VENV / "bin" / "python", VENV / "Scripts" / "python.exe"]: - if maybe.exists(): - return str(maybe) - raise Exception(f"Unable to locate Python binary in {VENV}") - - -def in_virtualenv() -> bool: - """Return true if running inside a virtualenv""" - return sys.prefix != sys.base_prefix - - -def create_venv( - path: Path, prompt: T.Union[str, None] = None, clear: bool = False -) -> str: - """ - Create a virtual env in the given path and return a pathlib.Path instance - representing the python executable. - """ - venv.create(str(path), clear=clear, with_pip=True, prompt=prompt) - return get_python_binary_from(path) - - -def _compile(PYTHON: str, *args): - cmd = [ - PYTHON, - "-m", - "piptools", - "compile", - "--resolver=backtracking", - "-qq", - "--allow-unsafe", - ] - cmd.extend(args) - subprocess.check_call(cmd, cwd=str(PROJECT_DIR)) - - -def _pip(PYTHON: str, *args): - cmd = [PYTHON, "-m", "pip", "-q"] - cmd.extend(args) - subprocess.check_call(cmd, cwd=str(PROJECT_DIR)) - - -def _sync(PYTHON: str, *args): - cmd = [PYTHON, "-m", "piptools", "sync", "-q"] - cmd.extend(args) - subprocess.check_call(cmd, cwd=str(PROJECT_DIR)) +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "storyville.settings") -def _compile_and_sync(PYTHON: str, args: T.Iterable): - """Run pip-compile on requirements.in and requirements-dev.in, then pip-sync""" - # Ensure pip and piptools are up to date in the venv - print("Checking for the latest pip updates.") - _pip(PYTHON, "install", "--upgrade", "pip", "pip-tools") - - # Then, use piptools sync to install requirements - print("Checking for requirements changes.") - _compile( - PYTHON, - "-o", - str(base_reqs.relative_to(PROJECT_DIR)), - *args, - str(base_in.relative_to(PROJECT_DIR)), - ) - _compile( - PYTHON, - "-o", - str(dev_reqs.relative_to(PROJECT_DIR)), - *args, - str(dev_in.relative_to(PROJECT_DIR)), - ) - print("Updating the current virtual environment.") - _sync(PYTHON, str(dev_reqs)) - - -class Commands: - """ - This class is a container for commands that are not part of Django, but that we want - to run occasionally. It exists here because it includes the setup command that we - run to setup our virtualenv and bootstrap it. - """ - - @staticmethod - def devsetup(pkg: str, args: T.Iterable): - # First, ensure we have a virtualenv and know where it is - VENV = PROJECT_DIR / ".venv" - if in_virtualenv(): - # Already running in the virtual env - VENV = Path(sys.prefix) - PYTHON = sys.executable - elif VENV.exists(): - # venv exists but we're not running in it - PYTHON = get_python_binary_from(VENV) - else: - print("Creating new virtual environment") - PYTHON = create_venv(VENV, prompt=pkg, clear=True) - - print(f"Using virtual environment in {VENV}") - - print("Installing dependencies. May take a bit. Grab a coffee.") - _compile_and_sync(PYTHON, args) - - # Finally, make sure we have .env file - # If no .env, copy example.env to .env - dotenv = PROJECT_DIR / ".env" - example = PROJECT_DIR / "example.env" - if not dotenv.exists() and example.exists(): - shutil.copy(example, dotenv) - - print( - "Setup complete. Activate your virtual environment, then update your database with:" - ) - print(" python manage.py migrate") - - @staticmethod - def pipsync(pkg: str, args: T.Iterable): - if not in_virtualenv(): - print("Activate your virtual environment before running this command.") - exit(1) - PYTHON = sys.executable - _compile_and_sync(PYTHON, args) - - -def django_command(): - """Run administrative tasks.""" +if __name__ == "__main__": try: from django.core.management import execute_from_command_line except ImportError as exc: raise ImportError( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" + "forget to activate a virtual environment?\n\n" ) from exc - execute_from_command_line(sys.argv) - -if __name__ == "__main__": - package = get_django_package() - os.environ.setdefault("DJANGO_SETTINGS_MODULE", f"{package}.settings") - command = sys.argv[1] # argv[0] is manage.py - if hasattr(Commands, command): - cmd = getattr(Commands, command) - cmd(package, args=sys.argv[2:]) - else: - django_command() + execute_from_command_line(sys.argv) diff --git a/pyproject.toml b/pyproject.toml index ff476a4..124dea6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,12 +28,12 @@ dependencies = [ ] # TODO Get setuptools build working -# [build-system] +[build-system] # With setuptools-scm, package data files (e.g. templates) that are tracked by git will # be automatically detected and included. Without setuptools-scm, you would need to # specify a file pattern in MANIFEST.in to collect them. -# requires = ["setuptools>=66.1.1", "setuptools-scm>=7.0.5", "wheel"] -# build-backend = "setuptools.build_meta" +requires = ["setuptools>=66.1.1", "setuptools-scm>=7.0.5", "wheel"] +build-backend = "setuptools.build_meta" [tool.setuptools.dynamic.version] attr = "storyville.__version__" diff --git a/storyville/__init__.py b/src/storyville/__init__.py similarity index 100% rename from storyville/__init__.py rename to src/storyville/__init__.py diff --git a/storyville/apps.py b/src/storyville/apps.py similarity index 100% rename from storyville/apps.py rename to src/storyville/apps.py diff --git a/storyville/asgi.py b/src/storyville/asgi.py similarity index 100% rename from storyville/asgi.py rename to src/storyville/asgi.py diff --git a/storyville/celery.py b/src/storyville/celery.py similarity index 100% rename from storyville/celery.py rename to src/storyville/celery.py diff --git a/storyville/fixtures/smoketest.json b/src/storyville/fixtures/smoketest.json similarity index 100% rename from storyville/fixtures/smoketest.json rename to src/storyville/fixtures/smoketest.json diff --git a/storyville/settings.py b/src/storyville/settings.py similarity index 96% rename from storyville/settings.py rename to src/storyville/settings.py index 2641f73..5886305 100644 --- a/storyville/settings.py +++ b/src/storyville/settings.py @@ -71,7 +71,7 @@ TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [BASE_DIR / "templates"], + "DIRS": [], "APP_DIRS": True, "OPTIONS": { "context_processors": [ @@ -105,7 +105,8 @@ # Get environment settings env = environ.Env() -DOTENV = BASE_DIR / ".env" +# Switched to src layout, so .env is now in the parent directory +DOTENV = BASE_DIR.parent / ".env" if DOTENV.exists() and not env("IGNORE_ENV_FILE", default=False): environ.Env.read_env(DOTENV) @@ -124,13 +125,13 @@ DATA_DIR = Path(env("DATA_DIR", default=BASE_DIR.joinpath("var"))) # Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/3.2/howto/static-files/ +# https://docs.djangoproject.com/en/4.2/howto/static-files/ STATIC_URL = "/static/" -STATIC_ROOT = DATA_DIR / "static" +STATIC_ROOT = DATA_DIR / "www" / "static" STATIC_ROOT.mkdir(parents=True, exist_ok=True) -STATICFILES_DIRS = [BASE_DIR / "static"] +# STATICFILES_DIRS = [BASE_DIR / "static"] MEDIA_URL = "/media/" -MEDIA_ROOT = DATA_DIR / "media" +MEDIA_ROOT = DATA_DIR / "www" / "media" MEDIA_ROOT.mkdir(parents=True, exist_ok=True) # ManifestStaticFilesStorage is recommended in production, to prevent outdated diff --git a/static/iqlovecraft/Digital-Patreon-Logo_FieryCoral.png b/src/storyville/static/iqlovecraft/Digital-Patreon-Logo_FieryCoral.png similarity index 100% rename from static/iqlovecraft/Digital-Patreon-Logo_FieryCoral.png rename to src/storyville/static/iqlovecraft/Digital-Patreon-Logo_FieryCoral.png diff --git a/static/iqlovecraft/Digital-Patreon-Wordmark_FieryCoral.png b/src/storyville/static/iqlovecraft/Digital-Patreon-Wordmark_FieryCoral.png similarity index 100% rename from static/iqlovecraft/Digital-Patreon-Wordmark_FieryCoral.png rename to src/storyville/static/iqlovecraft/Digital-Patreon-Wordmark_FieryCoral.png diff --git a/static/iqlovecraft/PatreonBanner-001.png b/src/storyville/static/iqlovecraft/PatreonBanner-001.png similarity index 100% rename from static/iqlovecraft/PatreonBanner-001.png rename to src/storyville/static/iqlovecraft/PatreonBanner-001.png diff --git a/static/mindvessel.com/mindvessel.css b/src/storyville/static/mindvessel.com/mindvessel.css similarity index 100% rename from static/mindvessel.com/mindvessel.css rename to src/storyville/static/mindvessel.com/mindvessel.css diff --git a/static/veselosky.me/CF-Banner.jpg b/src/storyville/static/veselosky.me/CF-Banner.jpg similarity index 100% rename from static/veselosky.me/CF-Banner.jpg rename to src/storyville/static/veselosky.me/CF-Banner.jpg diff --git a/static/veselosky.me/cover-PC1-CursingFate-portrait_medium.jpg b/src/storyville/static/veselosky.me/cover-PC1-CursingFate-portrait_medium.jpg similarity index 100% rename from static/veselosky.me/cover-PC1-CursingFate-portrait_medium.jpg rename to src/storyville/static/veselosky.me/cover-PC1-CursingFate-portrait_medium.jpg diff --git a/static/veselosky.me/cover-PC1-CursingFate.jpg b/src/storyville/static/veselosky.me/cover-PC1-CursingFate.jpg similarity index 100% rename from static/veselosky.me/cover-PC1-CursingFate.jpg rename to src/storyville/static/veselosky.me/cover-PC1-CursingFate.jpg diff --git a/static/veselosky.me/cover-PC2-ShiftingLoyalties-portrait_medium.jpg b/src/storyville/static/veselosky.me/cover-PC2-ShiftingLoyalties-portrait_medium.jpg similarity index 100% rename from static/veselosky.me/cover-PC2-ShiftingLoyalties-portrait_medium.jpg rename to src/storyville/static/veselosky.me/cover-PC2-ShiftingLoyalties-portrait_medium.jpg diff --git a/static/veselosky.me/cover-PC2-ShiftingLoyalties.jpg b/src/storyville/static/veselosky.me/cover-PC2-ShiftingLoyalties.jpg similarity index 100% rename from static/veselosky.me/cover-PC2-ShiftingLoyalties.jpg rename to src/storyville/static/veselosky.me/cover-PC2-ShiftingLoyalties.jpg diff --git a/static/veselosky.me/cover-PC3-SummoningCourage-portrait_medium.jpg b/src/storyville/static/veselosky.me/cover-PC3-SummoningCourage-portrait_medium.jpg similarity index 100% rename from static/veselosky.me/cover-PC3-SummoningCourage-portrait_medium.jpg rename to src/storyville/static/veselosky.me/cover-PC3-SummoningCourage-portrait_medium.jpg diff --git a/static/veselosky.me/cover-PC3-SummoningCourage.jpg b/src/storyville/static/veselosky.me/cover-PC3-SummoningCourage.jpg similarity index 100% rename from static/veselosky.me/cover-PC3-SummoningCourage.jpg rename to src/storyville/static/veselosky.me/cover-PC3-SummoningCourage.jpg diff --git a/static/veselosky.me/veselosky.css b/src/storyville/static/veselosky.me/veselosky.css similarity index 100% rename from static/veselosky.me/veselosky.css rename to src/storyville/static/veselosky.me/veselosky.css diff --git a/templates/iqlovecraft/index.html b/src/storyville/templates/iqlovecraft/index.html similarity index 100% rename from templates/iqlovecraft/index.html rename to src/storyville/templates/iqlovecraft/index.html diff --git a/templates/storyville/blocks/header_masthead.html b/src/storyville/templates/storyville/blocks/header_masthead.html similarity index 100% rename from templates/storyville/blocks/header_masthead.html rename to src/storyville/templates/storyville/blocks/header_masthead.html diff --git a/templates/veselosky.me/blocks/novels_card.html b/src/storyville/templates/veselosky.me/blocks/novels_card.html similarity index 100% rename from templates/veselosky.me/blocks/novels_card.html rename to src/storyville/templates/veselosky.me/blocks/novels_card.html diff --git a/templates/veselosky.me/index.html b/src/storyville/templates/veselosky.me/index.html similarity index 100% rename from templates/veselosky.me/index.html rename to src/storyville/templates/veselosky.me/index.html diff --git a/storyville/urls.py b/src/storyville/urls.py similarity index 100% rename from storyville/urls.py rename to src/storyville/urls.py diff --git a/storyville/wsgi.py b/src/storyville/wsgi.py similarity index 100% rename from storyville/wsgi.py rename to src/storyville/wsgi.py From fa1cc8e694c723235f2768b5901fe9af1bf311f3 Mon Sep 17 00:00:00 2001 From: Vince Veselosky Date: Thu, 26 Dec 2024 16:27:40 -0500 Subject: [PATCH 03/10] Use granian vs gunicorn as production wsgi server --- pyproject.toml | 7 +- src/storyville/fixtures/smoketest.json | 6 - src/storyville/settings.py | 11 +- uv.lock | 181 ++++++++++++++----------- 4 files changed, 107 insertions(+), 98 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 124dea6..6ca4ef1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,13 +13,10 @@ dependencies = [ "django-sitevars >= 1.0.2", "django-tinymce >= 3.4.0", "docutils >= 0.19", + "granian>=1.7.1", "pillow >= 10.0.1", + "redis>=5.2.1", "rich >= 13.3.3", - - # For production or any non-local-dev work: - "gunicorn >= 20.1.0", - "setproctitle >= 1.3.2", - # If using celery, add these: # celery >= 5.2.7 # django-celery-beat >= 2.5.0 diff --git a/src/storyville/fixtures/smoketest.json b/src/storyville/fixtures/smoketest.json index 0b2188c..66a6d25 100644 --- a/src/storyville/fixtures/smoketest.json +++ b/src/storyville/fixtures/smoketest.json @@ -699,7 +699,6 @@ "description": "Applying the Getting Things Done system to a digital lifestyle.", "share_image": 1, "base_template": "", - "content_template": "commoncontent/blocks/article_featuredimage.html", "body": "

David Allen's Getting Things Done: The Art of Stress-Free Productivity is a phenomenon in the tech community. If you're reading this blog, you've probably already read the book, or at least know something about the productivity system that it defines. I read it years ago, but like many readers never put into practice more than a tiny portion of the system.

\r\n

As 2012 drew to a close and I looked back on all the things I meant to accomplish, I decided that I should give this productivity bible another look, in the hopes of getting more things done in 2013. I won't bother to summarize the system that David Allen defines. The book is very readable and does a much better job than I could. Instead, I'm just going to note how I decided to apply the principles of his system in my own life, especially given the changes in technology and lifestyle since the book was originally published a dozen years ago.

\r\n

Some essential elements of the GTD system include:

\r\n
    \r\n
  • one or more inboxes (as few as you can get away with),
  • \r\n
  • a calendar,
  • \r\n
  • a \"tickler\" file to remind yourself of tasks that can only start at some future date,
  • \r\n
  • a place to organize your reference material, and
  • \r\n
  • a tool for organizing lists of projects and tasks.
  • \r\n
\r\n

The original GTD system was developed in a paper-focused world, before cloud-based calendars and Internet-connected phones became the norm. I've worked really hard to eliminate the mountains of paper in my life, so I have no interest in buying filing cabinets and manila folders.

\r\n

The vast majority of things I need to manage in my own life are actually digital. Email, digital music, more email, PDF downloads, email again, digital pictures, and did I mention the email? Digital references take up far less space, are easier to move, and are full-text searchable. I quickly resolved that my organization system would be digital, not physical.

\r\n

As I had already begun to use it for scribbling notes I wanted to keep track of, I decided to do the simplest digital thing that could work for staying organized, and elected Evernote as my default tool.

\r\n

Evernote gives me myriad advantages over a paper-based system.

\r\n
    \r\n
  • It's a great place to jot down random notes (replacing a physical notepad). It can even create notes from pictures or voice recordings, making it even easier to capture everything.
  • \r\n
  • The option to use notebooks and/or tags to organize notes, and its full-text search capability, make it an excellent reference database (replacing the filing cabinet).
  • \r\n
  • It runs on my phone, so it's always with me (no need to carry a paper organizer or notepad).
  • \r\n
  • It magically synchronizes with both my work computer and my home computer, so I never have to switch contexts to access it.
  • \r\n
\r\n

Setting up Evernote to work with my system was dead simple. I renamed the default notebook to \"INBOX.\" Any random notes I capture are there, waiting to be processed when next I process my inboxes. I created a Projects stack, containing a notebook for each large project, and a \"Miscellaneous\" notebook with a separate note for each smallish project. A Reference stack contains various notebooks organized by topic where I can file informational notes, PDF or Word documents, photos of the whiteboard scribbles from a brainstorming session, or any other assets I need to keep around.

\r\n

With a physical inbox, Allen recommends dealing with things that won't fit into it by writing a reminder of them on a piece of paper and placing the paper in the inbox. Since my inbox is digital, physical things won't fit into it. So if I need a reminder of a physical thing, I snap a picture of it with my phone and add it to my Evernote inbox. I also file away papers by scanning them or taking a picture with my phone (and then trashing/recycling the physical paper).

\r\n

Allen recommends keeping a list with all your \"next actions\" on it. I have become accustomed to visualizing work using a kanban system, so instead of a \"next actions\" list, I have a notebook called Backlog containing a note for each task, and another called WIP that contains notes for the tasks I am currently working on. When completed, I move them to the Done notebook. During my weekly review of open projects, I determine the next actions for each project and add a note for each one to the Backlog.

\r\n

GTD recommends keeping an agenda list for every regular meeting you have, so that you never have to be embarrassed that you forgot to ask Bob about that one thing when you spoke with him this morning. I keep an Agenda notebook in Evernote, with a separate note for each person or group I speak to regularly. If I run into someone in the hallway, I can whip out my phone and access their agenda immediately. Any notes generated from the meeting go back into the inbox to be processed.

\r\n

Since most of my reading is also digital, I use Pocket (formerly Read It Later) as my Reading list. I do a lot of my reading in the moment as a less-than-two-minute task, but when I need to queue something up, I toss it into Pocket. I am finding, however, that my appetite for reading later is a bit more ambitious than the amount of time \"later\" actually affords me. Perhaps I need to work on this.

\r\n

GTD Patterns I Don't Apply

\r\n

GTD recommends keeping your Next Action list organized by Context: Things you can do at home, at work, at the phone, etc. I found organizing by context to be almost useless, because almost all my tasks can be performed in any context. My work is all digital, and my work computer is a laptop I bring home with me at night. In a pinch, most of my work could performed on my phone. I always have a phone in my pocket, so there's no need for a \"calls\" context, I can make calls from anywhere. Most of my home activities are habits rather than tasks (take out trash, wash clothes, etc.) and therefore do not need to be tracked.

\r\n

I don't have a \"Waiting For\" notebook for tracking delegated tasks. Instead, I place a reminder on my calendar to follow up on a certain date if the awaited item has not arrived in my inbox by then. I also make \"appointments\" blocking out time to complete important tasks, otherwise there is a risk that my schedule will fill up and leave no time, or that I will get distracted by the in-the-moment work, leaving important things too late.

\r\n

My calendar is already digital and synchronized across my devices. I use the Exchange calendar provided by my company, but I could just as easily use a synchronized iCloud or Google Calendar.

\r\n

Finally, I decided that the tickler file was really an artifact of the paper world where a calendar is a sheet of paper with little boxes drawn on it. You can't file papers in those little boxes, so you need those 43 folders to store date-specific items. In my all-digital world, if I need to be reminded of something on a certain date, I can just drop it onto my digital calendar and store it there, or at worst store a link to some other repository. So I don't have a tickler notebook in Evernote, instead I use my calendar directly to fill that role.

\r\n

I'm just getting started using and tweaking this system, and I'm sure it will evolve over time. Perhaps I will write a follow-up post in a few months to record how it has changed and how effective it has been.

", "date_published": "2013-01-13T11:00:00Z", "date_modified": null, @@ -783,7 +782,6 @@ "description": "People like Dr. King are the most important people in America, people who serve as a national conscience, who remind us of the ideals we aspire to, and insist that we try harder to live up to them.", "share_image": 2, "base_template": "", - "content_template": "commoncontent/blocks/article_featuredimage.html", "body": "

As I write this, I sit in an apartment in Atlanta’s Old Fourth Ward, just a few blocks from where the Rev. Dr. Martin Luther King Jr. preached to his congregation. I cannot express in words the gratitude I feel toward Dr. King and all the thousands of people who marched with him to demand equal rights for all Americans. Since its birth, America has been a nation that aspired to high ideals of equality, and since its birth, America has struggled and failed to live up to those ideals. People like Dr. King are the most important people in America, people who serve as a national conscience, who remind us of the ideals we aspire to, and insist that we try harder to live up to them. Dr. King made us better as a nation.

\r\n

As the events of 2014 made painfully evident, although segregation and racism are no longer the law of the land, they often remain ingrained in the structure of our society. Too many people of color continue to struggle for fair treatment and fair opportunities. We have a long way to go, and a lot of work to do, before we can say we are living up to our American ideals.

\r\n

But growing up, as I did, in the South during the 1970s, attending elementary school in the recently desegregated school system, I learned what a huge impact a few dedicated people can have on society and culture. The cultural difference between the older generation segregationists and the children who were educated in integrated schools was nothing short of stunning to me. While racial prejudice has not disappeared from our culture, that early experience gives me hope that it really can.

\r\n

The Martin Luther King Jr. Holiday was set aside to “serve as a time for Americans to reflect on the principles of racial equality and nonviolent social change espoused by Martin Luther King, Jr.”

\r\n

More than a mere day of reflection, the King holiday has evolved into a national day of service toward the realization of his great dream. The video below explains the King legacy of service, and how you can honor his memory and your community through service.

\r\n

\r\n

MLK Day Legacy of Service

\r\n

Dr. King was a great orator, and although his written words are powerful, I don’t believe you can truly understand the power of those words unless you have heard them as he spoke them. It was not merely the words, but the passion of his presentation, that motivated everyday people to extraordinary action during the civil rights era. I believe everyone should take the time on Martin Luther King Jr. Day to listen to the man speak.

\r\n

If you have children, you owe it to them to teach them about Dr. King, about the struggle for racial equality, and about the nonviolent methods he used to create such great change. If we are going to make this world a better place for all of us, we need Dr. King’s leadership and ideals to live on in future generations.

\r\n

Here are some places to hear Dr. King speak.

\r\n", "date_published": "2015-01-19T11:00:00Z", "date_modified": null, @@ -1007,7 +1005,6 @@ "description": "", "share_image": 4, "base_template": "", - "content_template": "commoncontent/blocks/article_featuredimage.html", "body": "

Cursing Fate: The Piero Codex Book One, is now available for pre-order on most major ebook retailers! This is the first book in my urban fantasy thriller series, The Piero Codex. Books two and three will follow quickly in a rapid release schedule, so don't worry, you won't have to wait long to read the whole trilogy.

\r\n

I am unspeakably excited that my first book is finally available to the world. In fact, I'm so excited that you can call me Giveaway Bob, because I'm giving them away!

\r\n

For a limited time, just for being an early fan, you can get the

\r\n

Cursing Fate ebook for FREE! Click here to get it!

\r\n

And if you need convincing, check out this gorgeous cover (by Barb Hoeter at Coverinked)!

", "date_published": "2018-03-28T10:00:00Z", "date_modified": null, @@ -1035,7 +1032,6 @@ "description": "", "share_image": 5, "base_template": "", - "content_template": "commoncontent/blocks/article_featuredimage.html", "body": "

Big announcement! Shifting Loyalties: The Piero Codex Book Two is now available for pre-order on most major ebook retailers!

\r\n

Barb from CoverInked really stepped up her game on this cover, don't you think?

\r\n

When it’s a matter of life and death, how do you know who to trust?

\r\n

Mack was never good at trusting people, even before he went underground hiding from the Seers Guild. But when the former love of Mack’s life (Marina) goes missing, and an unknown organization makes dramatic moves against the Protectors of the Piero Codex, he needs help.

\r\n

Maybe he can trust Recca, but she still seems to have a hidden agenda of her own. Maybe he can trust Lilly, but will involving her just get her killed? Maybe he can trust Marina’s husband Richard, but how is he supposed to work with the guy who stole his girlfriend?

\r\n

With the enemy closing in, and time running out, turning an enemy agent seems like the best option. But when you can’t even trust your friends, how do you trust an enemy? Even if he manages to make all the right choices, how is Mack going to convince the other players to trust him long enough to save Marina and protect the Codex?

\r\n

This second installment in The Piero Codex series takes you deeper into the world of seers, and reveals some of the secrets of the Codex itself! Get it today! (Pre-orders will be delivered June 14, 2018.)

\r\n

Where To Get It

\r\n\r\n

 

", "date_published": "2018-05-09T10:00:00Z", "date_modified": null, @@ -1091,7 +1087,6 @@ "description": "", "share_image": 5, "base_template": "", - "content_template": "commoncontent/blocks/article_featuredimage.html", "body": "

You can stop holding your breath! Shifting Loyalties: The Piero Codex Book Two has been released! (Okay, I know you weren't really holding your breath, but I was!) This is my second novel, as well as being the second book in The Piero Codex series.

\r\n

I'm excited to introduce shape-shifters into the Seers Guild world with this release. You have some new characters to meet (including the mysterious ex, Marina), some secrets of the Codex itself will be revealed, and Mack is going to get into a whole new mess of trouble! This second installment has more action, more magic, more pages, and possibly more bourbon (I didn't count the drinks). Trust me, it's even better than the first one!

\r\n

If you pre-ordered the book, you should have it on your reading gizmo already. If you didn't, hit your favorite ebook retailer and grab it today (links below for your convenience)!

\r\n

I'm currently hard at work writing Summoning Courage: The Piero Codex Book Three to wrap up this trilogy. In the meantime, I hope you enjoy reading Shifting Loyalties!

", "date_published": "2018-06-14T10:00:00Z", "date_modified": null, @@ -1203,7 +1198,6 @@ "description": "", "share_image": 8, "base_template": "", - "content_template": "commoncontent/blocks/article_featuredimage.html", "body": "

According to Hofstadter's Law, every project takes longer than you expect it to (even when you take into account Hofstadter's Law), and my latest book project is no exception. But, as of today, Summoning Courage: The Piero Codex Book Three is available at ebook retailers everywhere!

\r\n

I am delighted to announce the release of this conclusion to The Piero Codex trilogy. These were my first novels, and it has been an incredible learning experience for me to produce them. Those lessons learned will make my next series even better.

\r\n

Now that the edits are done and the cover is attached, I'm going to take a little break from writing to try to close out another very late project: the remodel of Albatross House. I'm going to stay focused on that project until it's done, so I probably will not be releasing another book in 2019. However, I have about 80 thousand words already banked for my next series; there will definitely be another release in the future!

\r\n

Thank you for following along with my creative journey. I sincerely hope you are enjoying it!

\r\n

Your author friend, Vince Veselosky

", "date_published": "2019-04-30T10:00:00Z", "date_modified": null, diff --git a/src/storyville/settings.py b/src/storyville/settings.py index 5886305..e0971c9 100644 --- a/src/storyville/settings.py +++ b/src/storyville/settings.py @@ -115,7 +115,11 @@ # insecure configuration in production. SECRET_KEY = env("SECRET_KEY") DEBUG = env("DEBUG", default=False) -ALLOWED_HOSTS = env("ALLOWED_HOSTS", default=[]) +ALLOWED_HOSTS = env("ALLOWED_HOSTS", default=["localhost"]) +# If a site id is provided in the environment, use it. Otherwise, let Django +# look it up via the host header. +if site_id := env("SITE_ID", default=None, cast=int): + SITE_ID = site_id # Local data written by the app should be kept in one directory for ease of backup. # In DEV this can be a subdir of BASE_DIR. In production, for single-server setups @@ -225,17 +229,12 @@ # SECTION 3: DEVELOPMENT: If running in a dev environment, loosen restrictions # and add debugging tools. ####################################################################################### -SITE_ID = env("SITE_ID", default=None, cast=int) # Rich test output TEST_RUNNER = "django_rich.test.RichRunner" if DEBUG: ALLOWED_HOSTS = ["*"] - # So you don't have to add localhost and/or 127.0.0.1 to your Sites table: - # But note: if your Django project only serves one site, you can set this outside - # the DEBUG section. See README for details. - # SITE_ID = 1 if find_spec("debug_toolbar"): # Debug toolbar is optional diff --git a/uv.lock b/uv.lock index 433fa37..a0a2be9 100644 --- a/uv.lock +++ b/uv.lock @@ -22,6 +22,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918 }, ] +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233 }, +] + [[package]] name = "certifi" version = "2024.8.30" @@ -105,7 +114,7 @@ name = "click" version = "8.1.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } wheels = [ @@ -440,15 +449,50 @@ wheels = [ ] [[package]] -name = "gunicorn" -version = "23.0.0" +name = "granian" +version = "1.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "packaging" }, + { name = "click" }, + { name = "uvloop", marker = "platform_python_implementation == 'CPython' and sys_platform != 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031 } +sdist = { url = "https://files.pythonhosted.org/packages/52/3a/d88828fb585b7acad8a5ee8c36cbfbc860b4fc750315b5f95e3028087c2b/granian-1.7.1.tar.gz", hash = "sha256:d15d715063c1e6119438fa1449913673b28b220613062c7518fa3095fdc6b881", size = 80215 } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029 }, + { url = "https://files.pythonhosted.org/packages/6e/4f/18ccee38241efa631cd99e68704061faafb1de649b833f753d8fb78b299b/granian-1.7.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:fa52dc0785e23f9b899c712aa0695c2b320ddf1ac40671c997094e2a60334a3e", size = 2493561 }, + { url = "https://files.pythonhosted.org/packages/92/ca/655d2e92007dc85a8ff95624dd3df88f334e91e4ed89fa4f753bbcea484f/granian-1.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:60630626303cb3e4d2730ebf2b93a7f3536137b3c97c16d7c36a58d8e9ddb3bc", size = 2244479 }, + { url = "https://files.pythonhosted.org/packages/94/7d/6faa302354121b431fc32c74cbc378604bdb9df7f7f42083d87cd8ef55be/granian-1.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b51c706e1d19b273e615a1c714656c65d6922a3c7cee1aefd2d60e6a11da4416", size = 2228168 }, + { url = "https://files.pythonhosted.org/packages/30/f6/158106d14f3a5ebfdba1980bd0f1bd9065ca2f1f74780c12b67ab1751692/granian-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5e68182def3d5d0069d1ec66a3a16a89eabc28ff9d59bbaf213fb89195cec1b", size = 2360286 }, + { url = "https://files.pythonhosted.org/packages/38/6b/64f60eb81a1849d54ecfeb1e6a1d8591487c6fffbcd5b102332b0efaa8f8/granian-1.7.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:240c7afa77de8368f54c4cf2eb614151915bbd9ab47b28d68c19a23c54a6237a", size = 2412972 }, + { url = "https://files.pythonhosted.org/packages/1c/93/dcd358b35f3da5e78bf0564a086a23cda3e02f2e043e570af414a9ee4b8d/granian-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0eb53abaff555c0d637814d8f2f32155d30193d8c6d47c09d6004f26a3582ccb", size = 2528282 }, + { url = "https://files.pythonhosted.org/packages/6b/62/adf72a5cf4ea34ee64f4130c73abcd934f3f3491553781603a67feee5504/granian-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:37aaa001ef0e5930c1c135002617e53bdf8ef63fedd46c73fb7d8418e150a525", size = 2485077 }, + { url = "https://files.pythonhosted.org/packages/74/5e/f1d62230fb6b123fe641b9b46563be2207f962b33190d18bed5b2078522e/granian-1.7.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:fd5c38015a6a2ade9a78cc2db1b46af9d9390b551ac8d865553fee07d607c892", size = 2493310 }, + { url = "https://files.pythonhosted.org/packages/a7/0c/dced29be7e724251b23be92df2476703fc3763624c5a3bf90b28456b5c21/granian-1.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:424caab6b69c248bf10bda654ab28377769057d74d9c939f641078d1458d78f6", size = 2243903 }, + { url = "https://files.pythonhosted.org/packages/95/49/101f93828315c1dcbbdd1870d3df8af0689a36b73868439ab5f70de5917a/granian-1.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39a5d1186099e0e227477131809b598ce9ba16a8cda4f9ddee3e10274b2fc4d5", size = 2228266 }, + { url = "https://files.pythonhosted.org/packages/34/8e/2a192926a7478ae8084dd8c4f9f5f0f391c6496e64149987cc1786eab8f3/granian-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:594c21cc98f0d85d9c96b4232f2cf448ea8f85019d48a8f7cc2f7cae4bb88f2e", size = 2360187 }, + { url = "https://files.pythonhosted.org/packages/9a/6d/b7f10909772a429986f6d333157feba49e4e142fcb7b430ac5f4105260b8/granian-1.7.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:557db0c117b92476a6f081a8f1b97b7e7ca486ec6ec425b14d59b92a09e1bc2b", size = 2412953 }, + { url = "https://files.pythonhosted.org/packages/3a/fa/12003cbbc5db8df54553df75f27105d436b8afafad8f3c838e7de8746337/granian-1.7.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:11bfb4ce486aac0154416a254aadef7f0626359ffe19be21166b08b04160a6f4", size = 2528184 }, + { url = "https://files.pythonhosted.org/packages/5f/9f/938a98f9b329fe717d73b3ef79fb114de68bb6be3e9812d163bd0535cdfd/granian-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:1c1bde4adc55802970aa66bdc41e65e31c5e5c60eceb27ed16fed74d572f14b9", size = 2484447 }, + { url = "https://files.pythonhosted.org/packages/28/69/4fd93111f44f16908a483b0fcd4c00a0f42a70463c6bbcf16f3a04ee7664/granian-1.7.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0d8d2ff49b39008518c2828578ecec6f66e5679f7478089c793077becde7cd04", size = 2490168 }, + { url = "https://files.pythonhosted.org/packages/04/bd/006a3bf71821d3a8d17e3b56073b2003207f6b8a49350bbc6500f4fcc8fe/granian-1.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f07a8ef9e06189409fc0c496f600e9d759720043fd7e53da0d6ff7a1640f47f9", size = 2242702 }, + { url = "https://files.pythonhosted.org/packages/ee/2d/232174f6fce5c7d6e95a7f62e2a30abed86a0baae8da9bf44290f18d1a15/granian-1.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:492f4dc8fb795ea4651b5013bed058c7ae7fbad771277a7dfa7fbc5d4c8490eb", size = 2225635 }, + { url = "https://files.pythonhosted.org/packages/31/62/50ce5a5eb74562333db8c9ca181d70a316f5c73802c0044156053eb8544c/granian-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db4d3e4060c871d7ec12363c59af40beb71c573fb0a81029b97528930faf3488", size = 2357800 }, + { url = "https://files.pythonhosted.org/packages/9c/a0/4b3af959b8512eab3032477489c83602f1c3aaa851391edc3a5b66240d83/granian-1.7.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8bd8b333f80b0f413e5f16d5668732e31e0f84429a68e6c792d71b3f7867096c", size = 2405525 }, + { url = "https://files.pythonhosted.org/packages/a8/d2/1af39c983916c2ea54f384ba1de2b0eb2ac33993c168a865d4a82f9c62ea/granian-1.7.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8fd42175ab79dc607e8775de3195449ba6e0340154d5e968a8ff08b8f49cb7c0", size = 2526393 }, + { url = "https://files.pythonhosted.org/packages/53/1b/5325f4a63a5021bf720e7a30bbb2072dedc1e253f6fa072ffd074df781be/granian-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:be9db2b8406f4b57393414c9171b92a8c38eafec1fe0bb79bc19bbca9cf47c55", size = 2480368 }, + { url = "https://files.pythonhosted.org/packages/99/bf/0a724bcc4b8afbd23340e89ddf108b332c57839a609d893dd3a6e44a5d47/granian-1.7.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ea11fbe99f7046022ac5e3ac3b8448d89368d39eca34d7ab64706193485065ef", size = 2490301 }, + { url = "https://files.pythonhosted.org/packages/29/59/ebf4db5063447062e78f61953b019dfb73dd1de30d54a860da081f06c9bc/granian-1.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:af84050c58d7697dda1caf3f644a6c7eb6c11ff8ca1d561e23643d9d99016d05", size = 2242685 }, + { url = "https://files.pythonhosted.org/packages/75/82/ed4d9e20aef0f9924bf4130ceb581bd25b87ffd4c21fd792b8ce29f89488/granian-1.7.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e5814cf24e6179221db20fc34997be173568331b8561c3fdfcce93febe6f5d", size = 2225837 }, + { url = "https://files.pythonhosted.org/packages/24/9d/f731c92401648e238062de1b83230dbb2229dc24fb0c4f84139cfbb3502a/granian-1.7.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:527a20731d936213e108ae020f6fdc92c7aebe739787eba3050b6fcdad2b56f7", size = 2357522 }, + { url = "https://files.pythonhosted.org/packages/93/3c/432f15f6d88ba8e3febb0e63c85e8a84832efdd854522100f9eb20ba108f/granian-1.7.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:de17ba29b3ddf1e51a5d6c0205ca4c17c54e4c45a29427ce5f40b9d156415238", size = 2405493 }, + { url = "https://files.pythonhosted.org/packages/1d/89/e5e6d600e3fb9b9a671937fa65ce91968ff20a2d717a94b606cfea26f95f/granian-1.7.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:17e0b9551a51f71d7fb55290c4469fd8d256c7f9a0625b072758ae1a3feb0334", size = 2526267 }, + { url = "https://files.pythonhosted.org/packages/0f/cf/394bda56fb75575d6dab3d4ae104c1d5edb0fba812096d3732a1f6b2fbc8/granian-1.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:c71d3a4c7d1b6219682fa4c01125deac45596efe0603e38c5a29d635fc3c3363", size = 2480467 }, + { url = "https://files.pythonhosted.org/packages/01/02/0edff6499b13e0de110516b60fa64c5ee7e7285be404be1aa3efadc7d6ab/granian-1.7.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:fc2195ed7c369c59c81ec3b94c625ac061512a9e17b3d17c63cdc131307fd50a", size = 2242832 }, + { url = "https://files.pythonhosted.org/packages/47/55/066877feca2904745b6fc17378066e75f20a41bdd95960e3498997ef2c19/granian-1.7.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:0e97b62f241a3dc830de7ded811897ecce506909b507a56f6e8b02031462445c", size = 2035680 }, + { url = "https://files.pythonhosted.org/packages/09/85/cd3c54918bd88f210b10d6484289ef29aad81115a80ddd386d672bf0738b/granian-1.7.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd8292ed7d4293905420ca5468f85be6b1245a9214380c8a0df4fa864ebd6521", size = 2226780 }, + { url = "https://files.pythonhosted.org/packages/5d/0b/d8db66293616e9f8765819762533724479f9934881e2b744acc746b8b64b/granian-1.7.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e82d97e12bad15a2c14c2ff26f015f6e2099aa3e6fdea95a14202085f88e5345", size = 2358385 }, + { url = "https://files.pythonhosted.org/packages/12/8c/d28e2cff7ab0ec37981ab57cbfa23a25b2ea6611fd007973773235a83262/granian-1.7.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d5bd955228c848a238c131b104bee055e1a1245b3c699fb450c362538845728e", size = 2411296 }, + { url = "https://files.pythonhosted.org/packages/8b/e0/a425950e9ed513b7b8eff67c447806bbbbffef1c901a349efc3487e71eb5/granian-1.7.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:893a3e774796a86646eb4a5982a869e5c538ec3b874f868c3e7950f1aaedd92a", size = 2526510 }, + { url = "https://files.pythonhosted.org/packages/9e/92/e9910e1619fe262e1e15d16d55cdc5676604c2103526dccb5d443068cb20/granian-1.7.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0629cd2b2089fa9b24b6ffa69abe3f69708846b21dde05bec8d9df93e7bb7d6b", size = 2205458 }, ] [[package]] @@ -555,15 +599,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/0f/b5e545f0c7962be90366af3418989b12cf441d9da1e5d89d88f2f3e5cf8f/mistletoe-1.4.0-py3-none-any.whl", hash = "sha256:44a477803861de1237ba22e375c6b617690a31d2902b47279d1f8f7ed498a794", size = 51304 }, ] -[[package]] -name = "packaging" -version = "24.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, -] - [[package]] name = "parso" version = "0.8.4" @@ -768,6 +803,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, ] +[[package]] +name = "redis" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/da/d283a37303a995cd36f8b92db85135153dc4f7a8e4441aa827721b442cfb/redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f", size = 4608355 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/5f/fa26b9b2672cbe30e07d9a5bdf39cf16e3b80b42916757c5f92bca88e4ba/redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4", size = 261502 }, +] + [[package]] name = "regex" version = "2024.11.6" @@ -866,66 +913,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, ] -[[package]] -name = "setproctitle" -version = "1.3.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/4e/b09341b19b9ceb8b4c67298ab4a08ef7a4abdd3016c7bb152e9b6379031d/setproctitle-1.3.4.tar.gz", hash = "sha256:3b40d32a3e1f04e94231ed6dfee0da9e43b4f9c6b5450d53e6dd7754c34e0c50", size = 26456 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/f4/95937eb5c5370324a942ba90174c6d0fc7c5ad2f7f8ea989ccdbd6e1be5e/setproctitle-1.3.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0f6661a69c68349172ba7b4d5dd65fec2b0917abc99002425ad78c3e58cf7595", size = 16855 }, - { url = "https://files.pythonhosted.org/packages/32/a6/d49dbb0d75d02d11db49151469e1fee740efa45de7288bffcc4d88d0c290/setproctitle-1.3.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:754bac5e470adac7f7ec2239c485cd0b75f8197ca8a5b86ffb20eb3a3676cc42", size = 11627 }, - { url = "https://files.pythonhosted.org/packages/2e/cd/73a0fc913db50c3b736750ce67824f1108c2173e5d043a16ef9874b4f988/setproctitle-1.3.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7bc7088c15150745baf66db62a4ced4507d44419eb66207b609f91b64a682af", size = 31187 }, - { url = "https://files.pythonhosted.org/packages/63/0f/74f9112f7f506acc01f085811c6d135751b6fa42d30207f53b25579d043a/setproctitle-1.3.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a46ef3ecf61e4840fbc1145fdd38acf158d0da7543eda7b773ed2b30f75c2830", size = 32534 }, - { url = "https://files.pythonhosted.org/packages/3b/88/53eec2373745069d4c8a59d41ee2ef4a48949b77cccd0077c270261b238a/setproctitle-1.3.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffcb09d5c0ffa043254ec9a734a73f3791fec8bf6333592f906bb2e91ed2af1a", size = 29657 }, - { url = "https://files.pythonhosted.org/packages/50/1c/a4d3d8c20bf3bbafd8c5038e7da09043a9d21450b6a73694ada11c01b58a/setproctitle-1.3.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06c16b7a91cdc5d700271899e4383384a61aae83a3d53d0e2e5a266376083342", size = 30695 }, - { url = "https://files.pythonhosted.org/packages/a2/2a/9f290f0d10ea87a266d63025078eabfa040ad29ea10d815e167a5104de00/setproctitle-1.3.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9f9732e59863eaeedd3feef94b2b216cb86d40dda4fad2d0f0aaec3b31592716", size = 30340 }, - { url = "https://files.pythonhosted.org/packages/38/c4/5bfe02d4cdd16338973d452c7c6042abdd2827d90f7ce4e21bc003f2edb1/setproctitle-1.3.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e152f4ab9ea1632b5fecdd87cee354f2b2eb6e2dfc3aceb0eb36a01c1e12f94c", size = 29352 }, - { url = "https://files.pythonhosted.org/packages/b3/41/0dd85cef0e5a5a332bdda7b55738e950c2edecea3ae45c658990553d50f8/setproctitle-1.3.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:020ea47a79b2bbd7bd7b94b85ca956ba7cb026e82f41b20d2e1dac4008cead25", size = 31819 }, - { url = "https://files.pythonhosted.org/packages/d7/23/fbfacfed8805983a83324099484e47b9028d0d3c07a0fe017123eee3f580/setproctitle-1.3.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8c52b12b10e4057fc302bd09cb3e3f28bb382c30c044eb3396e805179a8260e4", size = 29745 }, - { url = "https://files.pythonhosted.org/packages/68/37/e18c5a00bfd1c4c2c815536d5c63a470e4364b571bd5096d38d0fe277bf5/setproctitle-1.3.4-cp310-cp310-win32.whl", hash = "sha256:a65a147f545f3fac86f11acb2d0b316d3e78139a9372317b7eb50561b2817ba0", size = 11358 }, - { url = "https://files.pythonhosted.org/packages/52/fd/1fae8c4c13af22d8d17816c44421085509a08dfa77f573d31447d6cd540c/setproctitle-1.3.4-cp310-cp310-win_amd64.whl", hash = "sha256:66821fada6426998762a3650a37fba77e814a249a95b1183011070744aff47f6", size = 12072 }, - { url = "https://files.pythonhosted.org/packages/5d/1a/1fb7d622195bcb3ce7b04366a833e51cfa5ad632c5dafe32e0763cd3fdc9/setproctitle-1.3.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f0f749f07002c2d6fecf37cedc43207a88e6c651926a470a5f229070cf791879", size = 16851 }, - { url = "https://files.pythonhosted.org/packages/46/54/e3aa4f46eddf795f10452ea878ff85c3496d36409636530f9a37e2de3cbe/setproctitle-1.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:90ea8d302a5d30b948451d146e94674a3c5b020cc0ced9a1c28f8ddb0f203a5d", size = 11620 }, - { url = "https://files.pythonhosted.org/packages/61/47/80988221679dfd93c464248abb71c2a96338f2ca3f8e3288d0ecb7422f4d/setproctitle-1.3.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f859c88193ed466bee4eb9d45fbc29d2253e6aa3ccd9119c9a1d8d95f409a60d", size = 31519 }, - { url = "https://files.pythonhosted.org/packages/2c/72/14984c127f708597e412f1a8cf7cac809b9bca50a267a6b01b221b094330/setproctitle-1.3.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3afa5a0ed08a477ded239c05db14c19af585975194a00adf594d48533b23701", size = 32860 }, - { url = "https://files.pythonhosted.org/packages/16/9d/34ea09295620fddae65cf7caeac81bbfc386a3ae6ce26a4dcadbb54c134d/setproctitle-1.3.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a78fce9018cc3e9a772b6537bbe3fe92380acf656c9f86db2f45e685af376e", size = 30029 }, - { url = "https://files.pythonhosted.org/packages/44/bf/a447a51054ceed23f69d4f7370289044b4508569f11da6db2eec087bc174/setproctitle-1.3.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d758e2eed2643afac5f2881542fbb5aa97640b54be20d0a5ed0691d02f0867d", size = 31017 }, - { url = "https://files.pythonhosted.org/packages/ec/46/adcffde6fb8d95458da0a568afdf0dabbbff6470299d94014676e1ab43c0/setproctitle-1.3.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ef133a1a2ee378d549048a12d56f4ef0e2b9113b0b25b6b77821e9af94d50634", size = 30762 }, - { url = "https://files.pythonhosted.org/packages/a3/cd/747a67ce1f6ef8fd1fa46b0b13ba0e007b80914bd549318830b8691ab9f6/setproctitle-1.3.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1d2a154b79d5fb42d1eff06e05e22f0e8091261d877dd47b37d31352b74ecc37", size = 29753 }, - { url = "https://files.pythonhosted.org/packages/3d/86/5939546e57238462a7839ae78399a635d1cfc5d125c7a12a28face111730/setproctitle-1.3.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:202eae632815571297833876a0f407d0d9c7ad9d843b38adbe687fe68c5192ee", size = 32161 }, - { url = "https://files.pythonhosted.org/packages/62/83/9194a4baed06e0e90a69e2e4a77a75e5a3ff008046870c79bc36a5c45e1c/setproctitle-1.3.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2b0080819859e80a7776ac47cf6accb4b7ad313baf55fabac89c000480dcd103", size = 30104 }, - { url = "https://files.pythonhosted.org/packages/ac/cd/08928fec23cbf4dae2a7b245b72d86e6458d64f4e7e6956cd80a9fda8c80/setproctitle-1.3.4-cp311-cp311-win32.whl", hash = "sha256:9c9d7d1267dee8c6627963d9376efa068858cfc8f573c083b1b6a2d297a8710f", size = 11349 }, - { url = "https://files.pythonhosted.org/packages/aa/19/240c4b99d57e045d3b2e2effa5924e810eabb18c56ef9c2336a7746dffe4/setproctitle-1.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:475986ddf6df65d619acd52188336a20f616589403f5a5ceb3fc70cdc137037a", size = 12071 }, - { url = "https://files.pythonhosted.org/packages/94/1f/02fb3c6038c819d86765316d2a911281fc56c7dd3a9355dceb3f26a5bf7b/setproctitle-1.3.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d06990dcfcd41bb3543c18dd25c8476fbfe1f236757f42fef560f6aa03ac8dfc", size = 16842 }, - { url = "https://files.pythonhosted.org/packages/b8/0c/d69e1f91c8f3d3aa74394e9e6ebb818f7d323e2d138ce1127e9462d09ebc/setproctitle-1.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:317218c9d8b17a010ab2d2f0851e8ef584077a38b1ba2b7c55c9e44e79a61e73", size = 11614 }, - { url = "https://files.pythonhosted.org/packages/86/ed/8031871d275302054b2f1b94b7cf5e850212cc412fe968f0979e64c1b838/setproctitle-1.3.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb5fefb53b9d9f334a5d9ec518a36b92a10b936011ac8a6b6dffd60135f16459", size = 31840 }, - { url = "https://files.pythonhosted.org/packages/45/b7/04f5d221cbdcff35d6cdf74e2a852e69dc8d8e746eb1b314be6b57b79c41/setproctitle-1.3.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0855006261635e8669646c7c304b494b6df0a194d2626683520103153ad63cc9", size = 33271 }, - { url = "https://files.pythonhosted.org/packages/25/b2/8dff0d2a72076e5535f117f33458d520538b5a0900b90a9f59a278f0d3f6/setproctitle-1.3.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a88e466fcaee659679c1d64dcb2eddbcb4bfadffeb68ba834d9c173a25b6184", size = 30509 }, - { url = "https://files.pythonhosted.org/packages/4b/cf/4f19cdc7fdff3eaeb3064ce6eeb27c63081dba3123fbf904ac6bf0de440c/setproctitle-1.3.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f963b6ed8ba33eda374a98d979e8a0eaf21f891b6e334701693a2c9510613c4c", size = 31543 }, - { url = "https://files.pythonhosted.org/packages/9b/a7/5f9c3c70dc5573f660f978fb3bb4847cd26ede95a5fc294d3f1cf6779800/setproctitle-1.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:122c2e05697fa91f5d23f00bbe98a9da1bd457b32529192e934095fadb0853f1", size = 31268 }, - { url = "https://files.pythonhosted.org/packages/26/ab/bbde90ea0ed6a062ef94fe1c609b68077f7eb586133a62fa62d0c8dd9f8c/setproctitle-1.3.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:1bba0a866f5895d5b769d8c36b161271c7fd407e5065862ab80ff91c29fbe554", size = 30232 }, - { url = "https://files.pythonhosted.org/packages/36/0e/817be9934eda4cf63c96c694c3383cb0d2e5d019a2871af7dbd2202f7a58/setproctitle-1.3.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:97f1f861998e326e640708488c442519ad69046374b2c3fe9bcc9869b387f23c", size = 32739 }, - { url = "https://files.pythonhosted.org/packages/b0/76/9b4877850c9c5f41c4bacae441285dead7c192bebf4fcbf3b3eb0e8033cc/setproctitle-1.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:726aee40357d4bdb70115442cb85ccc8e8bc554fc0bbbaa3a57cbe81df42287d", size = 30778 }, - { url = "https://files.pythonhosted.org/packages/b2/fa/bbc7ab32f253b9700ac20d78ba0d5fbdc4ea5789d33e1adb236cdf20b23a/setproctitle-1.3.4-cp312-cp312-win32.whl", hash = "sha256:04d6ba8b816dbb0bfd62000b0c3e583160893e6e8c4233e1dca1a9ae4d95d924", size = 11355 }, - { url = "https://files.pythonhosted.org/packages/44/5c/6e6665b5fd800206a9e537ab0d2630d7b9b31b4697d931ed468837cc9cf5/setproctitle-1.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:9c76e43cb351ba8887371240b599925cdf3ecececc5dfb7125c71678e7722c55", size = 12069 }, - { url = "https://files.pythonhosted.org/packages/d4/01/51d07ab1dbec8885ebad419d254c06b9e28f4363c163b737a89995a52b75/setproctitle-1.3.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d6e3b177e634aa6bbbfbf66d097b6d1cdb80fc60e912c7d8bace2e45699c07dd", size = 16831 }, - { url = "https://files.pythonhosted.org/packages/30/03/deff7089b525c0d8ec047e06661d2be67c87685a99be6a6aed2890b81c8f/setproctitle-1.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6b17655a5f245b416e127e02087ea6347a48821cc4626bc0fd57101bfcd88afc", size = 11607 }, - { url = "https://files.pythonhosted.org/packages/ea/be/cb2950b3f6ba460f530bda2c713828236c75d982d0aa0f62b33429a9b4d0/setproctitle-1.3.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa5057a86df920faab8ee83960b724bace01a3231eb8e3f2c93d78283504d598", size = 31881 }, - { url = "https://files.pythonhosted.org/packages/5c/b4/1f0dba7525a2fbefd08d4086e7e998d9c7581153807fb6b3083d06e0b8e2/setproctitle-1.3.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149fdfb8a26a555780c4ce53c92e6d3c990ef7b30f90a675eca02e83c6d5f76d", size = 33290 }, - { url = "https://files.pythonhosted.org/packages/2d/a8/07a160f9dcd1a7b1cad39ce6cbaf4425837502b0592a400c38cb21f0f247/setproctitle-1.3.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ded03546938a987f463c68ab98d683af87a83db7ac8093bbc179e77680be5ba2", size = 30489 }, - { url = "https://files.pythonhosted.org/packages/83/0c/3d972d9ea4165961a9764df5324d42bf2d059cb8a6ef516c67f068ed4d92/setproctitle-1.3.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ab9f5b7f2bbc1754bc6292d9a7312071058e5a891b0391e6d13b226133f36aa", size = 31576 }, - { url = "https://files.pythonhosted.org/packages/7a/c0/c12bdc2c91009defdd1b207ff156ccd691f5b9a6a0aae1ed9126d4ff9a0c/setproctitle-1.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0b19813c852566fa031902124336fa1f080c51e262fc90266a8c3d65ca47b74c", size = 31273 }, - { url = "https://files.pythonhosted.org/packages/4f/83/8d704bee57990b27537adf7c97540f32226ffa3922fb26bdd459da8a4470/setproctitle-1.3.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:db78b645dc63c0ccffca367a498f3b13492fb106a2243a1e998303ba79c996e2", size = 30236 }, - { url = "https://files.pythonhosted.org/packages/d8/42/94e31d1f515f831e1ae43f2405794257eb940a7972b2fbb6283790db2958/setproctitle-1.3.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b669aaac70bd9f03c070270b953f78d9ee56c4af6f0ff9f9cd3e6d1878c10b40", size = 32766 }, - { url = "https://files.pythonhosted.org/packages/83/53/01746ed8fb75239a001ee89d5eb8ad5a3022df510572d1cf60dd04567e13/setproctitle-1.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6dc3d656702791565994e64035a208be56b065675a5bc87b644c657d6d9e2232", size = 30812 }, - { url = "https://files.pythonhosted.org/packages/5f/ea/3ce61e70a6b898e95c0a1e393964c829103dc4ad4b0732cd70c8fc13e54c/setproctitle-1.3.4-cp313-cp313-win32.whl", hash = "sha256:091f682809a4d12291cf0205517619d2e7014986b7b00ebecfde3d76f8ae5a8f", size = 11349 }, - { url = "https://files.pythonhosted.org/packages/e7/1a/8149da1c19db6bd57164d62b1d91c188e7d77e695947cf1ac327c8aea513/setproctitle-1.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:adcd6ba863a315702184d92d3d3bbff290514f24a14695d310f02ae5e28bd1f7", size = 12062 }, - { url = "https://files.pythonhosted.org/packages/2f/d0/775418662081d44b91da236ed4503e10e7008c9c5fd30193e13db388fbef/setproctitle-1.3.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:939d364a187b2adfbf6ae488664277e717d56c7951a4ddeb4f23b281bc50bfe5", size = 11153 }, - { url = "https://files.pythonhosted.org/packages/fd/1f/b3b82633336cd9908bf74cbc06dd533025b3d3c202437c4e3d0bc871ca13/setproctitle-1.3.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb8a6a19be0cbf6da6fcbf3698b76c8af03fe83e4bd77c96c3922be3b88bf7da", size = 13310 }, - { url = "https://files.pythonhosted.org/packages/f5/89/887c6872ceed5ca344d25c8cc8a3f9b99bbcb25613c4b680476b29aabe23/setproctitle-1.3.4-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:779006f9e1aade9522a40e8d9635115ab15dd82b7af8e655967162e9c01e2573", size = 12911 }, - { url = "https://files.pythonhosted.org/packages/b0/8d/9e4a4651b1c5845a9aec0d2c08c65768ba5ca2ec76598124b45d384a5f46/setproctitle-1.3.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5519f2a7b8c535b0f1f77b30441476571373add72008230c81211ee17b423b57", size = 12105 }, -] - [[package]] name = "six" version = "1.17.0" @@ -961,7 +948,7 @@ wheels = [ [[package]] name = "storyville" version = "0.1.0" -source = { virtual = "." } +source = { editable = "." } dependencies = [ { name = "django" }, { name = "django-commoncontent" }, @@ -971,10 +958,10 @@ dependencies = [ { name = "django-sitevars" }, { name = "django-tinymce" }, { name = "docutils" }, - { name = "gunicorn" }, + { name = "granian" }, { name = "pillow" }, + { name = "redis" }, { name = "rich" }, - { name = "setproctitle" }, ] [package.dev-dependencies] @@ -995,10 +982,10 @@ requires-dist = [ { name = "django-sitevars", specifier = ">=1.0.2" }, { name = "django-tinymce", specifier = ">=3.4.0" }, { name = "docutils", specifier = ">=0.19" }, - { name = "gunicorn", specifier = ">=20.1.0" }, + { name = "granian", specifier = ">=1.7.1" }, { name = "pillow", specifier = ">=10.0.1" }, + { name = "redis", specifier = ">=5.2.1" }, { name = "rich", specifier = ">=13.3.3" }, - { name = "setproctitle", specifier = ">=1.3.2" }, ] [package.metadata.requires-dev] @@ -1053,7 +1040,7 @@ name = "tqdm" version = "4.67.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } wheels = [ @@ -1096,6 +1083,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, ] +[[package]] +name = "uvloop" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/76/44a55515e8c9505aa1420aebacf4dd82552e5e15691654894e90d0bd051a/uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f", size = 1442019 }, + { url = "https://files.pythonhosted.org/packages/35/5a/62d5800358a78cc25c8a6c72ef8b10851bdb8cca22e14d9c74167b7f86da/uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d", size = 801898 }, + { url = "https://files.pythonhosted.org/packages/f3/96/63695e0ebd7da6c741ccd4489b5947394435e198a1382349c17b1146bb97/uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26", size = 3827735 }, + { url = "https://files.pythonhosted.org/packages/61/e0/f0f8ec84979068ffae132c58c79af1de9cceeb664076beea86d941af1a30/uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb", size = 3825126 }, + { url = "https://files.pythonhosted.org/packages/bf/fe/5e94a977d058a54a19df95f12f7161ab6e323ad49f4dabc28822eb2df7ea/uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f", size = 3705789 }, + { url = "https://files.pythonhosted.org/packages/26/dd/c7179618e46092a77e036650c1f056041a028a35c4d76945089fcfc38af8/uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c", size = 3800523 }, + { url = "https://files.pythonhosted.org/packages/57/a7/4cf0334105c1160dd6819f3297f8700fda7fc30ab4f61fbf3e725acbc7cc/uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8", size = 1447410 }, + { url = "https://files.pythonhosted.org/packages/8c/7c/1517b0bbc2dbe784b563d6ab54f2ef88c890fdad77232c98ed490aa07132/uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0", size = 805476 }, + { url = "https://files.pythonhosted.org/packages/ee/ea/0bfae1aceb82a503f358d8d2fa126ca9dbdb2ba9c7866974faec1cb5875c/uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e", size = 3960855 }, + { url = "https://files.pythonhosted.org/packages/8a/ca/0864176a649838b838f36d44bf31c451597ab363b60dc9e09c9630619d41/uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb", size = 3973185 }, + { url = "https://files.pythonhosted.org/packages/30/bf/08ad29979a936d63787ba47a540de2132169f140d54aa25bc8c3df3e67f4/uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6", size = 3820256 }, + { url = "https://files.pythonhosted.org/packages/da/e2/5cf6ef37e3daf2f06e651aae5ea108ad30df3cb269102678b61ebf1fdf42/uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d", size = 3937323 }, + { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284 }, + { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349 }, + { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089 }, + { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770 }, + { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321 }, + { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022 }, + { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123 }, + { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325 }, + { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806 }, + { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068 }, + { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428 }, + { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018 }, +] + [[package]] name = "wcwidth" version = "0.2.13" From 32f06995a9554b16d8ad1682f2f69d4e76e963ba Mon Sep 17 00:00:00 2001 From: Vince Veselosky Date: Thu, 26 Dec 2024 16:34:08 -0500 Subject: [PATCH 04/10] Add Docker support --- .dockerignore | 27 ++++++++++++++++++ .vscode/launch.json | 17 +++++++++++- .vscode/tasks.json | 20 ++++++++++++++ Caddyfile | 29 +++++++++++++++++++ Dockerfile | 50 +++++++++++++++++++++++++++++++++ README.md | 60 ++++++++++++++++++++++++++-------------- compose.yml | 43 ++++++++++++++++++++++++++++ docker-compose.debug.yml | 12 ++++++++ 8 files changed, 237 insertions(+), 21 deletions(-) create mode 100644 .dockerignore create mode 100644 Caddyfile create mode 100644 Dockerfile create mode 100644 compose.yml create mode 100644 docker-compose.debug.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..edba5b6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,27 @@ +**/__pycache__ +**/.venv +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.tox +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE diff --git a/.vscode/launch.json b/.vscode/launch.json index e7aaf6a..544778c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,12 +6,27 @@ "configurations": [ { "name": "Runserver Django", - "type": "python", + "type": "debugpy", "request": "launch", "program": "${workspaceFolder}/manage.py", "args": ["runserver"], "django": true, "justMyCode": true + }, + { + "name": "Docker: Python - Django", + "type": "docker", + "request": "launch", + "preLaunchTask": "docker-run: debug", + "python": { + "pathMappings": [ + { + "localRoot": "${workspaceFolder}", + "remoteRoot": "/app" + } + ], + "projectType": "django" + } } ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 79c73b1..0f1c45a 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -20,6 +20,26 @@ "cwd": "${workspaceFolder}" }, "problemMatcher": [] + }, + { + "type": "docker-build", + "label": "docker-build", + "platform": "python", + "dockerBuild": { + "tag": "storyville:latest", + "dockerfile": "${workspaceFolder}/Dockerfile", + "context": "${workspaceFolder}", + "pull": true + } + }, + { + "type": "docker-run", + "label": "docker-run: debug", + "dependsOn": ["docker-build"], + "python": { + "args": ["runserver", "0.0.0.0:8000", "--nothreading", "--noreload"], + "file": "manage.py" + } } ] } diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..546c210 --- /dev/null +++ b/Caddyfile @@ -0,0 +1,29 @@ +# An example Caddyfile for serving the Django site (locally) + +# Specifying port 80 (or using the http:// prefix) tells Caddy not to bother with SSL +# certs from Let's Encrypt. This is better for local development. In production, use +# the actual domain name without a port number to get automatic SSL certs. +localhost:80 { + root * /var/www # Set working dir to /var/www for this site + + # Enable logging. Access logs go to stderr by default. This is what you want for + # contanerized applications, as logs are automatically collected by Docker/k8s. + log + + # Creates a matcher that matches any file that exists in the root. + @static file + # Serve files from the root directory if the request matches the @static matcher + # (that is, if they exist). + # Handle blocks are mutually exclusive, so the first one to match will be used, and + # the rest will be ignored. + handle @static { + file_server + } + + # The handle block without a matcher is the fallback. It will match any request that + # is not handled by previous handle blocks. Here, we reverse proxy to the Django app + # by default. + handle { + reverse_proxy http://storyville:8000 + } +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1ef9e8f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,50 @@ +# First, build the application in the `/app` directory using uv. +ARG PYTHON_VERSION=3.12 +FROM ghcr.io/astral-sh/uv:python${PYTHON_VERSION}-bookworm-slim AS builder +ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy +WORKDIR /app +# For good layer caching, create the virtualenv before copying the source code +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv sync --frozen --no-install-project --no-dev +# Production image doesn't need tests or other stuff at top level (but it DOES +# need the REAMDE.md because it's referenced in pyproject.toml) +COPY ./README.md /app/ +COPY ./src/ /app/ + +# Install the app itself +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv sync --frozen --no-dev + +# TODO If using i18n, compile the translations with the compilemessages command + +################################################################################ +# Then, use a final image without uv +FROM python:${PYTHON_VERSION}-slim-bookworm +# It is important to use the image that matches the builder, as the path to the +# Python executable must be the same, e.g., using `python:3.11-slim-bookworm` +# will fail. + +RUN useradd -r -U app +# Declare volumes that need to be mounted from the host. `db` holds the SQLite +# database, and `www` holds static files and uploaded media to be served by the +# web server. +VOLUME ["/app/var/db", "/app/var/www"] + +# Copy the application from the builder +COPY --from=builder --chown=app:app /app /app + +# Place executables in the environment at the front of the path +ENV PATH="/app/.venv/bin:$PATH" +# set environment variables +ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 DJANGO_SETTINGS_MODULE=storyville.settings + +# Switch to the app user +USER app + +# Use Granian to run the application +EXPOSE 8000 +CMD ["granian", "--interface", "wsgi", "--workers", "4", "--host", "0.0.0.0", "storyville.wsgi:application"] diff --git a/README.md b/README.md index 316f0f2..ccd3fd8 100644 --- a/README.md +++ b/README.md @@ -3,23 +3,43 @@ This Django project houses my actively maintained websites, and custom web-based tools I use. -## Dependency Management - -This project includes [pip-tools](https://pypi.org/project/pip-tools/) for dependency -management. There are two requirements files: `requirements.in` provides the acceptable -ranges of packages to install in a production environment (or any other environment); -`requirements-dev.in` provides packages to install in development environments. Both of -these have corresponding "pin" files: `requirements.txt` and `requirements-dev.txt`. - -To add a new dependency, add it to the correct `.in` file, and then run -`manage.py pipsync` to regenerate the pin files and synchronize your current virtual -environment with the new pin files. - -Any arguments passed to `manage.py pipsync` will be passed through to the underlying -`pip-compile` command. For example, to bump to the latest Django patch release use -`manage.py pipsync --upgrade-package django`. See the -[pip-tools docs](https://pypi.org/project/pip-tools/) for complete details. - -The pin files are not included in the template repository, but will be generated when -you run `manage.py devsetup`. This ensures you will get the latest version of Django and -related packages when starting a new project. +## Self-hosted Branch + +- [x] Export data from prod +- [x] Switch to pyproject.toml and uv for dependency management +- [x] Upgrade code to latest commoncontent and sitevars +- [x] Modify data export to fit new data model +- [x] Re-import data +- [x] Add smoke tests +- [x] Add pre-commit +- [x] Revert to stock manage.py +- [x] Switch to src layout and enable build backend in pyproject.toml +- [x] Add Dockerfile +- [x] Document ENV vars used by docker image +- [ ] Automate building container image on release +- [x] Update README + +## Environment: Local vs Docker + +Default values in the project's `settings.py` are suitable for running inside a Docker +container. For developing locally (running the app on local host and not in a Docker +container) copy the `example.env` file to `.env` within the project base directory and +adjust variables accordingly. + +To simplify both development and production, data files that need backing up are stored +together under a single directory, designated by the `DATA_DIR` environment variable. +The SQLite database files are stored under `$DATA_DIR/db/`. STATIC_ROOT and MEDIA_ROOT +are subdirectories stored under `$DATA_DIR/www`. + +In the Docker container, `DATA_DIR` has the default value of `/app/var/`. To run under +Docker, provide two volumes or bind mounts, one for `/app/var/db/`, and a second for +`/app/var/www/`. The www volume/mount must be shared with your web server to serve +static files and media. + +When running in a Docker container, several environment variables MUST be provided: + +- SECRET_KEY - App will not run without it. +- ALLOWED_HOSTS - For dev, set to "\*". For production, the sites you will serve. +- SITE_ID - Not required for production (assuming DNS is correct), but needed for local + dev. +- CACHE_URL - Using the example compose file, set to `redis://redis:6379/0` diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..0a2291b --- /dev/null +++ b/compose.yml @@ -0,0 +1,43 @@ +volumes: + redis_data: +services: + redis: + image: redis:7.4-alpine + command: redis-server --appendonly yes + # persistent storage + volumes: + - redis_data:/data + ports: + - "127.0.0.1:6379:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 30s + timeout: 10s + retries: 5 + storyville: + image: storyville:0.1.0 + build: + context: . + dockerfile: ./Dockerfile + ports: + - "8000:8000" + environment: + - CACHE_URL=redis://redis:6379/0 + - SECRET_KEY=Not a secret! Do not use in production! + - ALLOWED_HOSTS=* + - SITE_ID=3 + depends_on: + - redis + volumes: + - ./var/db:/app/var/db + - ./var/www:/app/var/www + caddy: + image: caddy:2.8.4 + ports: + - 80:80 + - 443:443 + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile + - ./var/www:/var/www + depends_on: + - storyville diff --git a/docker-compose.debug.yml b/docker-compose.debug.yml new file mode 100644 index 0000000..705ea91 --- /dev/null +++ b/docker-compose.debug.yml @@ -0,0 +1,12 @@ +version: '3.4' + +services: + storyville: + image: storyville + build: + context: . + dockerfile: ./Dockerfile + command: ["sh", "-c", "pip install debugpy -t /tmp && python /tmp/debugpy --wait-for-client --listen 0.0.0.0:5678 manage.py runserver 0.0.0.0:8000 --nothreading --noreload"] + ports: + - 8000:8000 + - 5678:5678 From b6cf1420fc77d4f3bc38ec5514d3ae68be423c3b Mon Sep 17 00:00:00 2001 From: Vince Veselosky Date: Sun, 29 Dec 2024 12:54:28 -0500 Subject: [PATCH 05/10] Add Dockerfile label for GHCR --- Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index 1ef9e8f..e8e63c0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -45,6 +45,9 @@ ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 DJANGO_SETTINGS_MODULE=storyvil # Switch to the app user USER app +# Add metadata +LABEL org.opencontainers.image.source=https://github.com/veselosky/storyville + # Use Granian to run the application EXPOSE 8000 CMD ["granian", "--interface", "wsgi", "--workers", "4", "--host", "0.0.0.0", "storyville.wsgi:application"] From 45d36b891be394f97ef111d7cc79c3efd1e3a6a2 Mon Sep 17 00:00:00 2001 From: Vince Veselosky Date: Sun, 29 Dec 2024 12:57:02 -0500 Subject: [PATCH 06/10] Updaet tox.ini --- tox.ini | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/tox.ini b/tox.ini index 0fdc757..72d7158 100644 --- a/tox.ini +++ b/tox.ini @@ -1,22 +1,23 @@ [tox] envlist = - {py38,py39,py310,py311,py312,py313}-django42 - py310-checks -skipsdist = true + {py312,py313}-django42 + py312-checks +; skipsdist = true skip_missing_interpreters = true [testenv] setenv = IGNORE_ENV_FILE=true SECRET_KEY="For testing only!" + DATA_DIR={toxinidir}/var commands = python manage.py collectstatic --noinput python -Wa manage.py test {posargs} deps = - -r pyproject.toml + . -[testenv:py310-checks] -basepython=python3.10 +[testenv:py312-checks] +basepython=python3.12 commands = ; Check model consistency and other bugs python manage.py check @@ -25,14 +26,14 @@ commands = ; Check whether you forgot to run makemigrations after changing models python manage.py makemigrations --no-input --dry-run --check deps = - -r pyproject.toml + . -[testenv:py310-coverage] -basepython=python3.10 +[testenv:py312-coverage] +basepython=python3.12 commands = coverage run --source='.' manage.py test {posargs} deps = - -r pyproject.toml + . coverage [coverage:run] @@ -47,16 +48,7 @@ omit = */wsgi.py **/migrations/* -[pycodestyle] -exclude = migrations -ignore = E203, E501, W503 -max-line-length = 88 - [gh-actions] python = - 3.8: py38 - 3.9: py39 - 3.10: py310 - 3.11: py311 3.12: py312 3.13: py313 From 20aa03f2a85a2fab3a3028602f9de78e8af488d1 Mon Sep 17 00:00:00 2001 From: Vince Veselosky Date: Sun, 29 Dec 2024 13:03:42 -0500 Subject: [PATCH 07/10] Update Github Actions workflows --- .github/workflows/release.yml | 4 ++-- .github/workflows/test.yml | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1bdc859..2a212af 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,13 +10,13 @@ jobs: uses: ./.github/workflows/test.yml create_release: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 needs: run_tests env: GH_TOKEN: ${{ github.token }} steps: - name: Check out the code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Create a Github Release run: gh release create ${{ github.ref }} --generate-notes diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dafc893..a185067 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,17 +6,16 @@ on: jobs: run_tests: - if: ${{ github.repository != 'veselosky/django-project-template' }} - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.12", "3.13"] steps: - name: Check out the code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: pip @@ -27,4 +26,4 @@ jobs: pip install build tox tox-gh-actions - name: Run the test suite - run: tox + run: tox run From 28e55679ab59b05963a9ae7267506909477a6889 Mon Sep 17 00:00:00 2001 From: Vince Veselosky Date: Sun, 29 Dec 2024 13:18:20 -0500 Subject: [PATCH 08/10] Debug toolbar urls should only be added if in INSTALLED_APPS --- src/storyville/urls.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/storyville/urls.py b/src/storyville/urls.py index 69228db..e810569 100644 --- a/src/storyville/urls.py +++ b/src/storyville/urls.py @@ -14,8 +14,6 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ -from importlib.util import find_spec - from commoncontent.sitemaps import sitemaps from commoncontent.views_optional import TinyMCEImageListView from django.conf import settings @@ -52,6 +50,6 @@ urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) # Add Django Debug Toolbar if installed - if find_spec("debug_toolbar"): + if "debug_toolbar" in settings.INSTALLED_APPS: # Catch-all patterns may block these if appended, hence insert urlpatterns.insert(0, path("__debug__/", include("debug_toolbar.urls"))) From 3d6ae32b19d12d126c729437f4210e9b6fbcf68b Mon Sep 17 00:00:00 2001 From: Vince Veselosky Date: Sun, 29 Dec 2024 13:18:44 -0500 Subject: [PATCH 09/10] Update README --- README.md | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index ccd3fd8..48ae7a8 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,6 @@ # Storyville: Vince's little corner of the web -This Django project houses my actively maintained websites, and custom web-based tools I -use. - -## Self-hosted Branch - -- [x] Export data from prod -- [x] Switch to pyproject.toml and uv for dependency management -- [x] Upgrade code to latest commoncontent and sitevars -- [x] Modify data export to fit new data model -- [x] Re-import data -- [x] Add smoke tests -- [x] Add pre-commit -- [x] Revert to stock manage.py -- [x] Switch to src layout and enable build backend in pyproject.toml -- [x] Add Dockerfile -- [x] Document ENV vars used by docker image -- [ ] Automate building container image on release -- [x] Update README +This Django project houses my actively maintained websites. ## Environment: Local vs Docker @@ -43,3 +26,5 @@ When running in a Docker container, several environment variables MUST be provid - SITE_ID - Not required for production (assuming DNS is correct), but needed for local dev. - CACHE_URL - Using the example compose file, set to `redis://redis:6379/0` + +TODO: Automate building container image on release From 700d459b619be9042c74e7a1bf0521e398c879ec Mon Sep 17 00:00:00 2001 From: Vince Veselosky Date: Sun, 29 Dec 2024 14:22:49 -0500 Subject: [PATCH 10/10] Skip recent images test if images are not present (e.g. in CI) --- tests/test_general.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_general.py b/tests/test_general.py index 4fcb958..827d622 100644 --- a/tests/test_general.py +++ b/tests/test_general.py @@ -2,6 +2,10 @@ "Smoke" tests to ensure that views load and templates render. """ +from pathlib import Path +from unittest import skipIf + +from django.conf import settings from django.test import TestCase, override_settings from django.urls import reverse @@ -30,6 +34,10 @@ def test_rss_feed(self): response = self.client.get(reverse("site_feed")) self.assertEqual(response.status_code, 200) + @skipIf( + not Path(settings.MEDIA_ROOT / "cover-PC3-SummoningCourage.jpg").exists(), + "No media files to test", + ) def test_tinymce_image_list(self): response = self.client.get(reverse("tinymce_image_list")) self.assertEqual(response.status_code, 200)