├── tests ├── __init__.py ├── urls.py ├── fixtures │ ├── empty.ts │ └── basic.ts ├── urls_basic.py ├── settings.py ├── logic--test-monkeypatching.py └── logic--test.py ├── justfile ├── typescript_routes ├── __init__.py ├── lib │ ├── __init__.py │ └── logic.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── generate_typescript_routes.py └── templates │ └── urls.ts.template ├── pytest.ini ├── scripts └── test ├── CHANGELOG.md ├── .gitignore ├── pyproject.toml ├── LICENSE ├── README.md └── uv.lock /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | test: 2 | scripts/test -------------------------------------------------------------------------------- /typescript_routes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /typescript_routes/lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | urlpatterns = [] 2 | -------------------------------------------------------------------------------- /typescript_routes/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /typescript_routes/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/empty.ts: -------------------------------------------------------------------------------- 1 | const URLS = { 2 | }; 3 | export default URLS; 4 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | python_files = *--test.py *--test-monkeypatching.py 3 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | export DJANGO_SETTINGS_MODULE=tests.settings 3 | uv run pytest "$@" 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.3.0 4 | 5 | - Migrated from `poetry` to `uv`. 6 | - Refactored the template to be amenable to packages that monkey-patch the template engine. 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Local development files 2 | /.env 3 | /.forge 4 | *.sqlite3 5 | 6 | # Publishing 7 | /dist 8 | 9 | # Python 10 | /.venv 11 | __pycache__/ 12 | *.py[cod] 13 | *$py.class 14 | 15 | # OS files 16 | .DS_Store 17 | -------------------------------------------------------------------------------- /tests/fixtures/basic.ts: -------------------------------------------------------------------------------- 1 | const URLS = { 2 | 'zod': (username: string) => `/zod/${username}/`, 3 | 'qux': (username: string) => `/qux/${username}/`, 4 | 'baz': (bar: string) => `/baz/${bar}/`, 5 | 'foo': (bar: number) => `/foo/${bar}/`, 6 | }; 7 | export default URLS; 8 | -------------------------------------------------------------------------------- /typescript_routes/templates/urls.ts.template: -------------------------------------------------------------------------------- 1 | const URLS = { 2 | {% for route in routes %} 3 | '{{ route.name }}': ({% for param in route.params %}{{ param.name }}: {{ param.typescript_type}}{% if not forloop.last%}, {% endif %}{% endfor %}) => `/{{ route.template }}`, 4 | {% endfor %} 5 | }; 6 | export default URLS; 7 | -------------------------------------------------------------------------------- /tests/urls_basic.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, re_path 2 | 3 | urlpatterns = [ 4 | path("foo//", lambda: None, name="foo"), 5 | path("baz//", lambda: None, name="baz"), 6 | path("qux//", lambda: None, name="qux"), 7 | re_path("zod/(?P\w+)/$/", lambda: None, name="zod"), 8 | ] 9 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | BASE_DIR = Path(__file__).parent.absolute() 4 | 5 | SECRET_KEY = "secret" 6 | 7 | DEBUG = True 8 | 9 | ROOT_URLCONF = "urls" 10 | 11 | USE_TZ = False 12 | 13 | INSTALLED_APPS = [ 14 | "typescript_routes", 15 | ] 16 | 17 | 18 | TEMPLATES = [ 19 | { 20 | "BACKEND": "django.template.backends.django.DjangoTemplates", 21 | "DIRS": ["templates"], 22 | "APP_DIRS": True, 23 | }, 24 | ] 25 | -------------------------------------------------------------------------------- /tests/logic--test-monkeypatching.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import django 4 | from typescript_routes.lib.logic import generate_routes 5 | 6 | def test_handles_monkeypatching() -> None: 7 | # See https://github.com/wrabit/django-cotton/issues/299. 8 | 9 | from django.template import base 10 | base.tag_re = re.compile(base.tag_re.pattern, re.DOTALL) 11 | 12 | django.setup() 13 | 14 | assert generate_routes("tests.urls_basic", []) == open("tests/fixtures/basic.ts").read() -------------------------------------------------------------------------------- /typescript_routes/management/commands/generate_typescript_routes.py: -------------------------------------------------------------------------------- 1 | from typescript_routes.lib.logic import generate_routes 2 | from django.core.management.base import BaseCommand 3 | from django.core.management.base import CommandParser 4 | 5 | 6 | class Command(BaseCommand): 7 | help = 'Prints a static TypeScript file that can be used to reverse Django URLs.' 8 | 9 | def add_arguments(self, parser: CommandParser) -> None: 10 | parser.add_argument("-u", "--urlconf", type=str) 11 | parser.add_argument("-i", "--ignore", nargs="*", type=str) 12 | 13 | def handle(self, *args, **options): 14 | urlconf = options["urlconf"] 15 | ignore = options["ignore"] or [] 16 | self.stdout.write(generate_routes(urlconf, ignore)) 17 | -------------------------------------------------------------------------------- /tests/logic--test.py: -------------------------------------------------------------------------------- 1 | from django.urls import get_resolver 2 | from typescript_routes.lib.logic import extract_routes, generate_routes 3 | 4 | def test_extract_routes_smoke() -> None: 5 | resolver = get_resolver("tests.urls") 6 | assert list(extract_routes(resolver, [])) == [] 7 | 8 | def test_generate_routes_smoke() -> None: 9 | import django 10 | django.setup() 11 | 12 | assert generate_routes("tests.urls", []) == open("tests/fixtures/empty.ts").read() 13 | 14 | 15 | def test_generate_routes_basic() -> None: 16 | import django 17 | django.setup() 18 | 19 | expectation = open("tests/fixtures/basic.ts").read() 20 | reality = generate_routes("tests.urls_basic", []) 21 | assert reality == expectation, f"Expected:\n{expectation}\n\nGot:\n{reality}" 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "django-typescript-routes" 3 | version = "0.3.0" 4 | description = "Generate typescript routes from a Django URLconf" 5 | authors = [{ name = "Justin Duke", email = "justin@buttondown.email" }] 6 | requires-python = "~=3.11" 7 | readme = "README.md" 8 | license = "MIT" 9 | dependencies = ["Django>=5,<6"] 10 | 11 | [project.urls] 12 | Repository = "https://github.com/buttondown-email/django-typescript-routes" 13 | 14 | [dependency-groups] 15 | dev = [ 16 | "pytest>=7.4.0,<8", 17 | "ruff>=0.0.284,<0.0.285", 18 | ] 19 | 20 | [tool.hatch.build.targets.sdist] 21 | include = ["typescript_routes"] 22 | 23 | [tool.hatch.build.targets.wheel] 24 | include = ["typescript_routes"] 25 | 26 | [build-system] 27 | requires = ["hatchling"] 28 | build-backend = "hatchling.build" 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023, Buttondown 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-typescript-routes 2 | 3 | Meant as a spiritual successor to [django-js-reverse](https://pypi.org/project/django-js-reverse/), `django-typescript-routes` is meant to answer to the following question: 4 | 5 | > I've got a Typescript-based SPA that is powered by a Django-based API. How do I safely make requests to Django without messing up the routes or parameters? 6 | 7 | `django-typescript-routes` is how! At a high level, it turns: 8 | 9 | ```python 10 | urls = [ 11 | path( 12 | r"about", 13 | about, 14 | name="about", 15 | ), 16 | path( 17 | r"/", 18 | subscribe, 19 | name="subscribe", 20 | ), 21 | path( 22 | r"//subscribers//success", 23 | subscription_success, 24 | name="subscription-success", 25 | ), 26 | ] 27 | ``` 28 | 29 | into: 30 | 31 | ```typescript 32 | const URLS = { 33 | about: () => `/`, 34 | subscribe: (username: string) => `/${username}`, 35 | "subscription-success": (username: string, pk: string) => 36 | `/${username}/subscribers/${pk}/success`, 37 | }; 38 | ``` 39 | 40 | ## Quick start 41 | 42 | 1. Install: 43 | 44 | ```bash 45 | uv add django-typescript-routes 46 | ``` 47 | 48 | 1. Add `django-typescript-routes` to your `INSTALLED_APPS` setting: 49 | 50 | ```python 51 | INSTALLED_APPS = [ 52 | ..., 53 | "typescript_routes", 54 | ... 55 | ] 56 | ``` 57 | 58 | 2. Run the management command to print out the typescript file: 59 | 60 | ```bash 61 | python manage.py generate_typescript_routes --urlconf projectname.urls > assets/urls.ts 62 | ``` 63 | 64 | ## Contributing 65 | 66 | ### Running the test suite 67 | 68 | Simply: 69 | 70 | ``` 71 | ./scripts/test 72 | ``` 73 | -------------------------------------------------------------------------------- /typescript_routes/lib/logic.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Iterable 3 | 4 | from django.template.loader import render_to_string 5 | from django.urls import URLResolver, get_resolver 6 | 7 | DJANGO_CONVERTER_NAME_TO_TYPESCRIPT_TYPE = { 8 | "StringConverter": "string", 9 | "IntConverter": "number", 10 | "UUIDConverter": "string", 11 | "SlugConverter": "string", 12 | } 13 | 14 | 15 | @dataclass 16 | class Parameter: 17 | name: str 18 | typescript_type: str 19 | 20 | 21 | @dataclass 22 | class Route: 23 | name: str 24 | params: list[Parameter] 25 | template: str 26 | 27 | 28 | def munge_template(raw_template: str, params: list[str]) -> str: 29 | text = raw_template 30 | for param in params: 31 | text = text.replace(f"%({param})s", "${{{}}}".format(param)) 32 | return text 33 | 34 | 35 | def extract_routes(resolver: URLResolver, denylist: list[str]) -> Iterable[Route]: 36 | # A lot of this approach is borrowed from `django-js-reverse`, just with a different 37 | # way of munging the final routes. 38 | keys = [key for key in resolver.reverse_dict.keys() if isinstance(key, str)] 39 | key_to_route = {key: resolver.reverse_dict.getlist(key)[0][0][0] for key in keys} 40 | for key in keys: 41 | path, parameter_keys = key_to_route[key] 42 | parameter_to_converter = resolver.reverse_dict.getlist(key)[0][3] 43 | params = [] 44 | for parameter_key in parameter_keys: 45 | if parameter_key not in parameter_to_converter: 46 | typescript_type = "string" 47 | else: 48 | converter = parameter_to_converter[parameter_key] 49 | converter_class_name = converter.__class__.__name__ 50 | typescript_type = DJANGO_CONVERTER_NAME_TO_TYPESCRIPT_TYPE.get( 51 | converter_class_name, "string" 52 | ) 53 | param = Parameter(name=parameter_key, typescript_type=typescript_type) 54 | params.append(param) 55 | yield Route(key, params, munge_template(path, parameter_keys)) 56 | for key, (prefix, subresolver) in resolver.namespace_dict.items(): 57 | if key in denylist: 58 | continue 59 | for route in extract_routes(subresolver, denylist): 60 | yield Route( 61 | f"{key}:{route.name}", route.params, f"{prefix}{route.template}" 62 | ) 63 | 64 | 65 | def generate_routes(urlconf: str, denylist: list[str]) -> str: 66 | resolver = get_resolver(urlconf) 67 | routes = extract_routes(resolver, denylist) 68 | rendered_string = render_to_string("urls.ts.template", {"routes": routes}) 69 | # Remove all empty lines injected via the template. 70 | rendered_string = "\n".join(line for line in rendered_string.split("\n") if line.strip()) 71 | return rendered_string.strip() + "\n" 72 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 2 3 | requires-python = ">=3.11, <4" 4 | 5 | [[package]] 6 | name = "asgiref" 7 | version = "3.8.1" 8 | source = { registry = "https://pypi.org/simple" } 9 | sdist = { url = "https://files.pythonhosted.org/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590", size = 35186, upload-time = "2024-03-22T14:39:36.863Z" } 10 | wheels = [ 11 | { url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828, upload-time = "2024-03-22T14:39:34.521Z" }, 12 | ] 13 | 14 | [[package]] 15 | name = "colorama" 16 | version = "0.4.6" 17 | source = { registry = "https://pypi.org/simple" } 18 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } 19 | wheels = [ 20 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, 21 | ] 22 | 23 | [[package]] 24 | name = "django" 25 | version = "5.1.2" 26 | source = { registry = "https://pypi.org/simple" } 27 | dependencies = [ 28 | { name = "asgiref" }, 29 | { name = "sqlparse" }, 30 | { name = "tzdata", marker = "sys_platform == 'win32'" }, 31 | ] 32 | sdist = { url = "https://files.pythonhosted.org/packages/9c/e5/a06e20c963b280af4aa9432bc694fbdeb1c8df9e28c2ffd5fbb71c4b1bec/Django-5.1.2.tar.gz", hash = "sha256:bd7376f90c99f96b643722eee676498706c9fd7dc759f55ebfaf2c08ebcdf4f0", size = 10711674, upload-time = "2024-10-08T14:53:12.217Z" } 33 | wheels = [ 34 | { url = "https://files.pythonhosted.org/packages/a3/b8/f205f2b8c44c6cdc555c4f56bbe85ceef7f67c0cf1caa8abe078bb7e32bd/Django-5.1.2-py3-none-any.whl", hash = "sha256:f11aa87ad8d5617171e3f77e1d5d16f004b79a2cf5d2e1d2b97a6a1f8e9ba5ed", size = 8276058, upload-time = "2024-10-08T14:53:05.58Z" }, 35 | ] 36 | 37 | [[package]] 38 | name = "django-typescript-routes" 39 | version = "0.2.0" 40 | source = { editable = "." } 41 | dependencies = [ 42 | { name = "django" }, 43 | ] 44 | 45 | [package.dev-dependencies] 46 | dev = [ 47 | { name = "pytest" }, 48 | { name = "ruff" }, 49 | ] 50 | 51 | [package.metadata] 52 | requires-dist = [{ name = "django", specifier = ">=5,<6" }] 53 | 54 | [package.metadata.requires-dev] 55 | dev = [ 56 | { name = "pytest", specifier = ">=7.4.0,<8" }, 57 | { name = "ruff", specifier = ">=0.0.284,<0.0.285" }, 58 | ] 59 | 60 | [[package]] 61 | name = "iniconfig" 62 | version = "2.0.0" 63 | source = { registry = "https://pypi.org/simple" } 64 | sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" } 65 | wheels = [ 66 | { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" }, 67 | ] 68 | 69 | [[package]] 70 | name = "packaging" 71 | version = "24.1" 72 | source = { registry = "https://pypi.org/simple" } 73 | sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788, upload-time = "2024-06-09T23:19:24.956Z" } 74 | wheels = [ 75 | { url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985, upload-time = "2024-06-09T23:19:21.909Z" }, 76 | ] 77 | 78 | [[package]] 79 | name = "pluggy" 80 | version = "1.5.0" 81 | source = { registry = "https://pypi.org/simple" } 82 | sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } 83 | wheels = [ 84 | { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, 85 | ] 86 | 87 | [[package]] 88 | name = "pytest" 89 | version = "7.4.4" 90 | source = { registry = "https://pypi.org/simple" } 91 | dependencies = [ 92 | { name = "colorama", marker = "sys_platform == 'win32'" }, 93 | { name = "iniconfig" }, 94 | { name = "packaging" }, 95 | { name = "pluggy" }, 96 | ] 97 | sdist = { url = "https://files.pythonhosted.org/packages/80/1f/9d8e98e4133ffb16c90f3b405c43e38d3abb715bb5d7a63a5a684f7e46a3/pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", size = 1357116, upload-time = "2023-12-31T12:00:18.035Z" } 98 | wheels = [ 99 | { url = "https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8", size = 325287, upload-time = "2023-12-31T12:00:13.963Z" }, 100 | ] 101 | 102 | [[package]] 103 | name = "ruff" 104 | version = "0.0.284" 105 | source = { registry = "https://pypi.org/simple" } 106 | sdist = { url = "https://files.pythonhosted.org/packages/ce/8a/f69eb801a82c6192ac3e8ed135dc0055f65fa7cee7798008f1a75b54945e/ruff-0.0.284.tar.gz", hash = "sha256:ebd3cc55cd499d326aac17a331deaea29bea206e01c08862f9b5c6e93d77a491", size = 1460991, upload-time = "2023-08-09T19:03:45.622Z" } 107 | wheels = [ 108 | { url = "https://files.pythonhosted.org/packages/48/47/b6562cbe5b20984662011f27a8349215145a167940f8188041b50ba3799b/ruff-0.0.284-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:8b949084941232e2c27f8d12c78c5a6a010927d712ecff17231ee1a8371c205b", size = 5511521, upload-time = "2023-08-09T19:03:08.081Z" }, 109 | { url = "https://files.pythonhosted.org/packages/cb/72/162879b9bf8b23749465299d7c46781ede5ebd05302127556b5a217d43fc/ruff-0.0.284-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:a3930d66b35e4dc96197422381dff2a4e965e9278b5533e71ae8474ef202fab0", size = 10624330, upload-time = "2023-08-09T19:03:10.598Z" }, 110 | { url = "https://files.pythonhosted.org/packages/67/f4/e9551e84f68988545101bbc363a6f0eec62c269be4df7632c6cdd7d9d929/ruff-0.0.284-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d1f7096038961d8bc3b956ee69d73826843eb5b39a5fa4ee717ed473ed69c95", size = 5375119, upload-time = "2023-08-09T19:03:13.344Z" }, 111 | { url = "https://files.pythonhosted.org/packages/e2/c5/8fc7057de618e6602a371687f34515aa6ca6282255a5f9dfd5b6734d4162/ruff-0.0.284-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bcaf85907fc905d838f46490ee15f04031927bbea44c478394b0bfdeadc27362", size = 5080890, upload-time = "2023-08-09T19:03:15.347Z" }, 112 | { url = "https://files.pythonhosted.org/packages/21/d3/08984f8493156f2b9cfc86159075d5116dce861ddda2715b9b125e5cb3e5/ruff-0.0.284-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3660b85a9d84162a055f1add334623ae2d8022a84dcd605d61c30a57b436c32", size = 5504895, upload-time = "2023-08-09T19:03:17.754Z" }, 113 | { url = "https://files.pythonhosted.org/packages/73/41/f75d55d75cc53c3230ec3bff6325e9aef0e5daea5d176eee4993eb252fdc/ruff-0.0.284-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0a3218458b140ea794da72b20ea09cbe13c4c1cdb7ac35e797370354628f4c05", size = 6119472, upload-time = "2023-08-09T19:03:20.098Z" }, 114 | { url = "https://files.pythonhosted.org/packages/9e/29/895765d896970b2fe633651a86a57f25baafc5b058b74b70592e8b31df09/ruff-0.0.284-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2fe880cff13fffd735387efbcad54ba0ff1272bceea07f86852a33ca71276f4", size = 6002432, upload-time = "2023-08-09T19:03:22.462Z" }, 115 | { url = "https://files.pythonhosted.org/packages/93/dd/7dc6e56cbf0dfb8cea86f60f26cb511f12c9694c5fffc4ef5b0a6701111f/ruff-0.0.284-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1d098ea74d0ce31478765d1f8b4fbdbba2efc532397b5c5e8e5ea0c13d7e5ae", size = 6914846, upload-time = "2023-08-09T19:03:24.925Z" }, 116 | { url = "https://files.pythonhosted.org/packages/bc/da/75543925543cb000cfc39c1ca1b4cd1b4bef39868da32d79b36de6fcb27d/ruff-0.0.284-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4c79ae3308e308b94635cd57a369d1e6f146d85019da2fbc63f55da183ee29b", size = 5721708, upload-time = "2023-08-09T19:03:27.657Z" }, 117 | { url = "https://files.pythonhosted.org/packages/dc/29/7f75eb219effb8f256adc5be879af863494733ffe94b63ac522aa7134b85/ruff-0.0.284-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f86b2b1e7033c00de45cc176cf26778650fb8804073a0495aca2f674797becbb", size = 5331664, upload-time = "2023-08-09T19:03:30.096Z" }, 118 | { url = "https://files.pythonhosted.org/packages/f2/03/d25a0266abc3df6267589d86330214e1d6d2583f1c94206039209a8c0af9/ruff-0.0.284-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e37e086f4d623c05cd45a6fe5006e77a2b37d57773aad96b7802a6b8ecf9c910", size = 5080917, upload-time = "2023-08-09T19:03:32.522Z" }, 119 | { url = "https://files.pythonhosted.org/packages/c8/bb/85f6415f87d3300f2719095a8dd379484da042a76d58a4fef04ba9fc98d6/ruff-0.0.284-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d29dfbe314e1131aa53df213fdfea7ee874dd96ea0dd1471093d93b59498384d", size = 5378354, upload-time = "2023-08-09T19:03:34.847Z" }, 120 | { url = "https://files.pythonhosted.org/packages/46/c6/1b9d40fbcdd2e8a5a1fbef92f0d1c5c55b632f9d36e704fdb169508632fd/ruff-0.0.284-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:88295fd649d0aa1f1271441df75bf06266a199497afd239fd392abcfd75acd7e", size = 5780152, upload-time = "2023-08-09T19:03:36.994Z" }, 121 | { url = "https://files.pythonhosted.org/packages/d7/04/e05129235c6b71f843a812808d939dda30c0874fea1c12007061a260025a/ruff-0.0.284-py3-none-win32.whl", hash = "sha256:735cd62fccc577032a367c31f6a9de7c1eb4c01fa9a2e60775067f44f3fc3091", size = 5265695, upload-time = "2023-08-09T19:03:39.243Z" }, 122 | { url = "https://files.pythonhosted.org/packages/8a/19/dee5d945772de39830cdacfff5b6ee274968e8e09c233d9ea282818b3b84/ruff-0.0.284-py3-none-win_amd64.whl", hash = "sha256:f67ed868d79fbcc61ad0fa034fe6eed2e8d438d32abce9c04b7c4c1464b2cf8e", size = 5609985, upload-time = "2023-08-09T19:03:41.558Z" }, 123 | { url = "https://files.pythonhosted.org/packages/21/a0/4392c875edd69441587e17a7041bf0bfc893d684b4e4d9166d44d7a7b7c0/ruff-0.0.284-py3-none-win_arm64.whl", hash = "sha256:1292cfc764eeec3cde35b3a31eae3f661d86418b5e220f5d5dba1c27a6eccbb6", size = 5405208, upload-time = "2023-08-09T19:03:44.104Z" }, 124 | ] 125 | 126 | [[package]] 127 | name = "sqlparse" 128 | version = "0.5.1" 129 | source = { registry = "https://pypi.org/simple" } 130 | sdist = { url = "https://files.pythonhosted.org/packages/73/82/dfa23ec2cbed08a801deab02fe7c904bfb00765256b155941d789a338c68/sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e", size = 84502, upload-time = "2024-07-15T19:30:27.085Z" } 131 | wheels = [ 132 | { url = "https://files.pythonhosted.org/packages/5d/a5/b2860373aa8de1e626b2bdfdd6df4355f0565b47e51f7d0c54fe70faf8fe/sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4", size = 44156, upload-time = "2024-07-15T19:30:25.033Z" }, 133 | ] 134 | 135 | [[package]] 136 | name = "tzdata" 137 | version = "2024.2" 138 | source = { registry = "https://pypi.org/simple" } 139 | sdist = { url = "https://files.pythonhosted.org/packages/e1/34/943888654477a574a86a98e9896bae89c7aa15078ec29f490fef2f1e5384/tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", size = 193282, upload-time = "2024-09-23T18:56:46.89Z" } 140 | wheels = [ 141 | { url = "https://files.pythonhosted.org/packages/a6/ab/7e5f53c3b9d14972843a647d8d7a853969a58aecc7559cb3267302c94774/tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd", size = 346586, upload-time = "2024-09-23T18:56:45.478Z" }, 142 | ] 143 | --------------------------------------------------------------------------------