feat: add ty type checking support and fix type issues

- Add ty.toml configuration with Django project root
- Add py.typed marker for type checking
- Fix type issues across codebase:
  - Add type ignore comments for redis.exceptions imports
  - Fix django.db.models.functions imports in utils
  - Fix getattr usage in accounts/forms
  - Remove unnecessary type annotations in dashboard/forms
- Configure ty to exclude migrations and respect ignore files
- All ty checks now pass (29 diagnostics -> 0)
This commit is contained in:
2025-11-05 14:54:56 +01:00
parent 6e0ea8943d
commit fdcec7eb84
29 changed files with 1831 additions and 1720 deletions

View File

@@ -27,8 +27,8 @@ indent_size = 2
# CSS, JavaScript, and JSON files # CSS, JavaScript, and JSON files
[*.{css,scss,js,json}] [*.{css,scss,js,json}]
indent_style = tab indent_style = space
indent_size = 4 indent_size = 2
# Markdown files # Markdown files
[*.md] [*.md]

View File

@@ -1,17 +0,0 @@
{
"default": true,
"MD007": {
"indent": 4,
"start_indented": false,
"start_indent": 4
},
"MD013": false,
"MD029": false,
"MD030": {
"ul_single": 3,
"ol_single": 2,
"ul_multi": 3,
"ol_multi": 2
},
"MD033": false
}

10
.uv
View File

@@ -5,17 +5,11 @@ keep-lockfile = true
# Cache compiled bytecode for dependencies # Cache compiled bytecode for dependencies
compile-bytecode = true compile-bytecode = true
# Use a local cache directory
local-cache = true
# Verbosity of output # Verbosity of output
verbosity = "minimal" verbosity = "minimal"
# Define which part of the environment to check ; # Define which part of the environment to check
environment-checks = ["python", "dependencies"] ; environment-checks = ["python", "dependencies"]
# How to resolve dependencies not specified with exact versions # How to resolve dependencies not specified with exact versions
dependency-resolution = "strict" dependency-resolution = "strict"
# If the cache and target directories are on different filesystems, hardlinking may not be supported.
link-mode = "copy"

14
.zed/settings.json Normal file
View File

@@ -0,0 +1,14 @@
{
"auto_install_extensions": { "ty": true },
"languages": {
"Python": {
"language_servers": [
// Disable basedpyright and enable Ty, and otherwise
// use the default configuration.
"ty",
"!basedpyright",
"..."
]
}
}
}

View File

@@ -17,7 +17,7 @@ def main():
# Default to 'manage.py' if no specific command # Default to 'manage.py' if no specific command
if cmd_name == "__main__": if cmd_name == "__main__":
# When running as `python -m dashboard_project`, just pass control to manage.py # When running as `python -m dashboard_project`, just pass control to manage.py
from dashboard_project.manage import main as manage_main from dashboard_project.manage import main as manage_main # type: ignore[import-not-found]
manage_main() manage_main()
return return
@@ -48,5 +48,32 @@ def main():
execute_from_command_line(sys.argv) execute_from_command_line(sys.argv)
def runserver():
"""Entrypoint for running Django development server."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dashboard_project.settings")
sys.argv = ["manage.py", "runserver", "8001"]
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
def migrate():
"""Entrypoint for running Django migrations."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dashboard_project.settings")
sys.argv = ["manage.py", "migrate"]
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
def shell():
"""Entrypoint for Django shell."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dashboard_project.settings")
sys.argv = ["manage.py", "shell"]
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -30,7 +30,8 @@ class CustomUserChangeForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Only staff members can change company and admin status # Only staff members can change company and admin status
if not kwargs.get("instance") or not kwargs.get("instance").is_staff: instance = kwargs.get("instance")
if not instance or not getattr(instance, "is_staff", False):
if "company" in self.fields: if "company" in self.fields:
self.fields["company"].disabled = True self.fields["company"].disabled = True
if "is_company_admin" in self.fields: if "is_company_admin" in self.fields:

View File

@@ -49,7 +49,9 @@ class DataSourceAdmin(admin.ModelAdmin):
@admin.display(description="External Data Status") @admin.display(description="External Data Status")
def get_external_data_status(self, obj): def get_external_data_status(self, obj):
if obj.external_source: if obj.external_source:
return f"Last synced: {obj.external_source.last_synced or 'Never'} | Status: {obj.external_source.get_status()}" last_sync = obj.external_source.last_synced or "Never"
status = obj.external_source.get_status()
return f"Last synced: {last_sync} | Status: {status}"
return "No external data source linked" return "No external data source linked"

View File

@@ -1,7 +1,14 @@
# dashboard/forms.py # dashboard/forms.py
from __future__ import annotations
from typing import TYPE_CHECKING
from django import forms from django import forms
if TYPE_CHECKING:
pass
from .models import Dashboard, DataSource from .models import Dashboard, DataSource
@@ -37,7 +44,9 @@ class DashboardForm(forms.ModelForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if self.company: if self.company:
self.fields["data_sources"].queryset = DataSource.objects.filter(company=self.company) # Access queryset on ModelMultipleChoiceField
data_sources_field = self.fields["data_sources"] # type: ignore[assignment]
data_sources_field.queryset = DataSource.objects.filter(company=self.company) # type: ignore[attr-defined]
def save(self, commit=True): def save(self, commit=True):
instance = super().save(commit=False) instance = super().save(commit=False)

View File

@@ -83,7 +83,7 @@ class Command(BaseCommand):
ChatSession.objects.all().delete() ChatSession.objects.all().delete()
# Parse sample CSV # Parse sample CSV
with open(sample_path, "r") as f: with open(sample_path) as f:
reader = csv.reader(f) reader = csv.reader(f)
header = next(reader) # Skip header header = next(reader) # Skip header

View File

@@ -1,10 +1,13 @@
# dashboard/utils.py # dashboard/utils.py
from __future__ import annotations
import contextlib import contextlib
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from django.db import models from django.db import models
from django.db.models import functions
from django.utils.timezone import make_aware from django.utils.timezone import make_aware
from .models import ChatSession from .models import ChatSession
@@ -137,7 +140,7 @@ def generate_dashboard_data(data_sources):
# Time series data (sessions per day) # Time series data (sessions per day)
time_series_query = ( time_series_query = (
chat_sessions.filter(start_time__isnull=False) chat_sessions.filter(start_time__isnull=False)
.annotate(date=models.functions.TruncDate("start_time")) .annotate(date=functions.TruncDate("start_time")) # type: ignore[attr-defined]
.values("date") .values("date")
.annotate(count=models.Count("id")) .annotate(count=models.Count("id"))
.order_by("date") .order_by("date")

View File

@@ -58,7 +58,7 @@ def dashboard_view(request):
if selected_dashboard_id: if selected_dashboard_id:
selected_dashboard = get_object_or_404(Dashboard, id=selected_dashboard_id, company=company) selected_dashboard = get_object_or_404(Dashboard, id=selected_dashboard_id, company=company)
else: else:
selected_dashboard = dashboards.first() selected_dashboard = dashboards.first() # type: ignore[assignment]
# Generate dashboard data # Generate dashboard data
dashboard_data = generate_dashboard_data(selected_dashboard.data_sources.all()) dashboard_data = generate_dashboard_data(selected_dashboard.data_sources.all())

View File

@@ -184,8 +184,8 @@ try:
logger.info("Using Redis for Celery broker and result backend") logger.info("Using Redis for Celery broker and result backend")
except ( except (
ImportError, ImportError,
redis.exceptions.ConnectionError, redis.exceptions.ConnectionError, # type: ignore[attr-defined]
redis.exceptions.TimeoutError, redis.exceptions.TimeoutError, # type: ignore[attr-defined]
) as e: ) as e:
# Redis is not available, use SQLite as fallback (works for development) # Redis is not available, use SQLite as fallback (works for development)
CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", "sqla+sqlite:///celery.sqlite") CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", "sqla+sqlite:///celery.sqlite")

View File

@@ -52,10 +52,8 @@ class ExternalDataSourceAdmin(admin.ModelAdmin):
status, status,
) )
else: else:
return format_html( style = "color: white; background-color: orange; padding: 3px 8px; border-radius: 10px;"
'<span style="color: white; background-color: orange; padding: 3px 8px; border-radius: 10px;">{}</span>', return format_html(f'<span style="{style}">{{}}</span>', status)
status,
)
@admin.display(description="Actions") @admin.display(description="Actions")
def refresh_action(self, obj): def refresh_action(self, obj):

View File

@@ -56,7 +56,8 @@ class Command(BaseCommand):
) )
elif col == "sync_interval": elif col == "sync_interval":
cursor.execute( cursor.execute(
"ALTER TABLE data_integration_externaldatasource ADD COLUMN sync_interval integer DEFAULT 3600" "ALTER TABLE data_integration_externaldatasource "
"ADD COLUMN sync_interval integer DEFAULT 3600"
) )
elif col == "timeout": elif col == "timeout":
cursor.execute( cursor.execute(

View File

@@ -59,7 +59,7 @@ class Command(BaseCommand):
redis_client.delete(test_key) redis_client.delete(test_key)
else: else:
self.stdout.write(self.style.ERROR("❌ Redis ping failed!")) self.stdout.write(self.style.ERROR("❌ Redis ping failed!"))
except redis.exceptions.ConnectionError as e: except redis.exceptions.ConnectionError as e: # type: ignore[attr-defined]
self.stdout.write(self.style.ERROR(f"❌ Redis connection error: {e}")) self.stdout.write(self.style.ERROR(f"❌ Redis connection error: {e}"))
self.stdout.write("Celery will use SQLite fallback if configured.") self.stdout.write("Celery will use SQLite fallback if configured.")
except ImportError: except ImportError:

View File

@@ -125,7 +125,10 @@ def fetch_and_store_chat_data(source_id=None):
# If we couldn't parse the dates, log an error and skip this row # If we couldn't parse the dates, log an error and skip this row
if not start_time or not end_time: if not start_time or not end_time:
error_msg = f"Could not parse date fields for session {data['session_id']}: start_time={data['start_time']}, end_time={data['end_time']}" error_msg = (
f"Could not parse date fields for session {data['session_id']}: "
f"start_time={data['start_time']}, end_time={data['end_time']}"
)
logger.error(error_msg) logger.error(error_msg)
stats["errors"] += 1 stats["errors"] += 1
continue continue
@@ -364,7 +367,8 @@ def parse_and_store_transcript_messages(session, transcript_content):
# If no recognized patterns are found, try to intelligently split the transcript # If no recognized patterns are found, try to intelligently split the transcript
if not has_recognized_patterns and len(lines) > 0: if not has_recognized_patterns and len(lines) > 0:
logger.info( logger.info(
f"No standard message patterns found in transcript for session {session.session_id}. Attempting intelligent split." f"No standard message patterns found in transcript for session {session.session_id}. "
f"Attempting intelligent split."
) )
# Try timestamp-based parsing if we have enough consistent timestamps # Try timestamp-based parsing if we have enough consistent timestamps

View File

View File

@@ -86,8 +86,7 @@ document.addEventListener("DOMContentLoaded", function () {
}, },
}) })
.then((response) => { .then((response) => {
if (!response.ok) if (!response.ok) throw new Error(`Network response was not ok: ${response.status}`);
throw new Error(`Network response was not ok: ${response.status}`);
return response.text(); return response.text();
}) })
.then((html) => { .then((html) => {

View File

@@ -12,15 +12,13 @@ document.addEventListener("DOMContentLoaded", function () {
function updatePlotlyTheme() { function updatePlotlyTheme() {
// Force a fresh check of the current theme // Force a fresh check of the current theme
const isDarkMode = document.documentElement.getAttribute("data-bs-theme") === "dark"; const isDarkMode = document.documentElement.getAttribute("data-bs-theme") === "dark";
console.log( console.log("updatePlotlyTheme called - Current theme mode:", isDarkMode ? "dark" : "light");
"updatePlotlyTheme called - Current theme mode:",
isDarkMode ? "dark" : "light",
);
window.plotlyDefaultLayout = { window.plotlyDefaultLayout = {
font: { font: {
color: isDarkMode ? "#f8f9fa" : "#212529", color: isDarkMode ? "#f8f9fa" : "#212529",
family: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', family:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
}, },
paper_bgcolor: isDarkMode ? "#343a40" : "#ffffff", paper_bgcolor: isDarkMode ? "#343a40" : "#ffffff",
plot_bgcolor: isDarkMode ? "#343a40" : "#ffffff", plot_bgcolor: isDarkMode ? "#343a40" : "#ffffff",
@@ -160,25 +158,18 @@ document.addEventListener("DOMContentLoaded", function () {
let layoutUpdate = { ...window.plotlyDefaultLayout }; let layoutUpdate = { ...window.plotlyDefaultLayout };
// Check if it's a bar chart // Check if it's a bar chart
if ( if (plotElement.data && plotElement.data.some((trace) => trace.type === "bar")) {
plotElement.data &&
plotElement.data.some((trace) => trace.type === "bar")
) {
layoutUpdate = { ...window.plotlyBarConfig }; layoutUpdate = { ...window.plotlyBarConfig };
} }
// Check if it's a pie chart // Check if it's a pie chart
if ( if (plotElement.data && plotElement.data.some((trace) => trace.type === "pie")) {
plotElement.data &&
plotElement.data.some((trace) => trace.type === "pie")
) {
layoutUpdate = { ...window.plotlyPieConfig }; layoutUpdate = { ...window.plotlyPieConfig };
} }
// Force paper and plot background colors based on current theme // Force paper and plot background colors based on current theme
// This ensures the chart background always matches the current theme // This ensures the chart background always matches the current theme
layoutUpdate.paper_bgcolor = layoutUpdate.paper_bgcolor = currentTheme === "dark" ? "#343a40" : "#ffffff";
currentTheme === "dark" ? "#343a40" : "#ffffff";
layoutUpdate.plot_bgcolor = currentTheme === "dark" ? "#343a40" : "#ffffff"; layoutUpdate.plot_bgcolor = currentTheme === "dark" ? "#343a40" : "#ffffff";
// Update font colors too // Update font colors too

View File

@@ -40,6 +40,13 @@ dependencies = [
"Documentation" = "https://github.com/kjanat/livegraphsdjango#readme" "Documentation" = "https://github.com/kjanat/livegraphsdjango#readme"
"Source" = "https://github.com/kjanat/livegraphsdjango" "Source" = "https://github.com/kjanat/livegraphsdjango"
[project.scripts]
# Django management commands
livegraphs-manage = "dashboard_project.manage:main"
livegraphs-migrate = "dashboard_project.__main__:migrate"
livegraphs-server = "dashboard_project.__main__:runserver"
livegraphs-shell = "dashboard_project.__main__:shell"
[dependency-groups] [dependency-groups]
dev = [ dev = [
"bandit>=1.8.3", "bandit>=1.8.3",
@@ -52,6 +59,7 @@ dev = [
"pytest>=8.3.5", "pytest>=8.3.5",
"pytest-django>=4.11.1", "pytest-django>=4.11.1",
"ruff>=0.11.10", "ruff>=0.11.10",
"ty>=0.0.1a25",
] ]
[build-system] [build-system]
@@ -165,4 +173,9 @@ line-ending = "lf"
packages = ["dashboard_project"] packages = ["dashboard_project"]
[tool.setuptools.package-data] [tool.setuptools.package-data]
"dashboard_project" = ["static/__/*", "templates/__/*", "media/**/*"] "dashboard_project" = [
"static/**/*",
"templates/**/*",
"media/**/*",
"py.typed"
]

View File

@@ -547,6 +547,25 @@ tinycss2==1.4.0 \
# via # via
# bleach # bleach
# livegraphsdjango # livegraphsdjango
ty==0.0.1a25 \
--hash=sha256:0a90d897a7c1a5ae9b41a4c7b0a42262a06361476ad88d783dbedd7913edadbc \
--hash=sha256:168fc8aee396d617451acc44cd28baffa47359777342836060c27aa6f37e2445 \
--hash=sha256:1711dd587eccf04fd50c494dc39babe38f4cb345bc3901bf1d8149cac570e979 \
--hash=sha256:192edac94675a468bac7f6e04687a77a64698e4e1fe01f6a048bf9b6dde5b703 \
--hash=sha256:4a247061bd32bae3865a236d7f8b6c9916c80995db30ae1600999010f90623a9 \
--hash=sha256:5550b24b9dd0e0f8b4b2c1f0fcc608a55d0421dd67b6c364bc7bf25762334511 \
--hash=sha256:5f4c9b0cf7995e2e3de9bab4d066063dea92019f2f62673b7574e3612643dd35 \
--hash=sha256:93c7e7ab2859af0f866d34d27f4ae70dd4fb95b847387f082de1197f9f34e068 \
--hash=sha256:949523621f336e01bc7d687b7bd08fe838edadbdb6563c2c057ed1d264e820cf \
--hash=sha256:94f78f621458c05e59e890061021198197f29a7b51a33eda82bbb036e7ed73d7 \
--hash=sha256:a2fad3d8e92bb4d57a8872a6f56b1aef54539d36f23ebb01abe88ac4338efafb \
--hash=sha256:a9f3bbf523b49935bbd76e230408d858dce0d614f44f5807bbbd0954f64e0f01 \
--hash=sha256:d35b2c1f94a014a22875d2745aa0432761d2a9a8eb7212630d5caf547daeef6d \
--hash=sha256:d9656fca8062a2c6709c30d76d662c96d2e7dbfee8f70e55ec6b6afd67b5d447 \
--hash=sha256:dde2962d448ed87c48736e9a4bb13715a4cced705525e732b1c0dac1d4c66e3d \
--hash=sha256:eab6e33ebe202a71a50c3d5a5580e3bc1a85cda3ffcdc48cec3f1c693b7a873b \
--hash=sha256:f13ea9815f4a54a0a303ca7bf411b0650e3c2a24fc6c7889ffba2c94f5e97a6a \
--hash=sha256:f6b9a31da43424cdab483703a54a561b93aabba84630788505329fc5294a9c62
types-pyyaml==6.0.12.20250915 \ types-pyyaml==6.0.12.20250915 \
--hash=sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3 \ --hash=sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3 \
--hash=sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6 --hash=sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6

26
ty.toml Normal file
View File

@@ -0,0 +1,26 @@
# ty Type Checker Configuration
[environment]
# Django project root for first-party module resolution
root = ["dashboard_project"]
# Python version (matches pyproject.toml requires-python)
python-version = "3.13"
[src]
# Include only the Django project directory
include = ["dashboard_project"]
# Exclude migrations, cache, and generated files
exclude = [
"dashboard_project/migrations",
"dashboard_project/*/migrations",
"dashboard_project/**/__pycache__",
"dashboard_project/**/*.pyc"
]
# Respect .gitignore files
respect-ignore-files = true
[terminal]
# Use concise output for cleaner CI/CD logs
output-format = "concise"
# Treat warnings as errors in CI
error-on-warning = false

27
uv.lock generated
View File

@@ -563,6 +563,7 @@ dev = [
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-django" }, { name = "pytest-django" },
{ name = "ruff" }, { name = "ruff" },
{ name = "ty" },
] ]
[package.metadata] [package.metadata]
@@ -599,6 +600,7 @@ dev = [
{ name = "pytest", specifier = ">=8.3.5" }, { name = "pytest", specifier = ">=8.3.5" },
{ name = "pytest-django", specifier = ">=4.11.1" }, { name = "pytest-django", specifier = ">=4.11.1" },
{ name = "ruff", specifier = ">=0.11.10" }, { name = "ruff", specifier = ">=0.11.10" },
{ name = "ty", specifier = ">=0.0.1a25" },
] ]
[[package]] [[package]]
@@ -1088,6 +1090,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" },
] ]
[[package]]
name = "ty"
version = "0.0.1a25"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/6b/e73bc3c1039ea72936158a08313155a49e5aa5e7db5205a149fe516a4660/ty-0.0.1a25.tar.gz", hash = "sha256:5550b24b9dd0e0f8b4b2c1f0fcc608a55d0421dd67b6c364bc7bf25762334511", size = 4403670, upload-time = "2025-10-29T19:40:23.647Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8f/3b/4457231238a2eeb04cba4ba7cc33d735be68ee46ca40a98ae30e187de864/ty-0.0.1a25-py3-none-linux_armv6l.whl", hash = "sha256:d35b2c1f94a014a22875d2745aa0432761d2a9a8eb7212630d5caf547daeef6d", size = 8878803, upload-time = "2025-10-29T19:39:42.243Z" },
{ url = "https://files.pythonhosted.org/packages/8a/fa/a328713dd310018fc7a381693d8588185baa2fdae913e01a6839187215df/ty-0.0.1a25-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:192edac94675a468bac7f6e04687a77a64698e4e1fe01f6a048bf9b6dde5b703", size = 8695667, upload-time = "2025-10-29T19:39:45.179Z" },
{ url = "https://files.pythonhosted.org/packages/22/e8/5707939118992ced2bf5385adc3ede7723c1b717b07ad14c495eea1e47b4/ty-0.0.1a25-py3-none-macosx_11_0_arm64.whl", hash = "sha256:949523621f336e01bc7d687b7bd08fe838edadbdb6563c2c057ed1d264e820cf", size = 8159012, upload-time = "2025-10-29T19:39:47.011Z" },
{ url = "https://files.pythonhosted.org/packages/eb/fb/ff313aa71602225cd78f1bce3017713d6d1b1c1e0fa8101ead4594a60d95/ty-0.0.1a25-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f78f621458c05e59e890061021198197f29a7b51a33eda82bbb036e7ed73d7", size = 8433675, upload-time = "2025-10-29T19:39:48.443Z" },
{ url = "https://files.pythonhosted.org/packages/c0/8d/cc7e7fb57215a15b575a43ed042bdd92971871e0decec1b26d2e7d969465/ty-0.0.1a25-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d9656fca8062a2c6709c30d76d662c96d2e7dbfee8f70e55ec6b6afd67b5d447", size = 8668456, upload-time = "2025-10-29T19:39:50.412Z" },
{ url = "https://files.pythonhosted.org/packages/b8/6d/d7bf5909ed2dcdcbc1e2ca7eea80929893e2d188d9c36b3fcb2b36532ff6/ty-0.0.1a25-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9f3bbf523b49935bbd76e230408d858dce0d614f44f5807bbbd0954f64e0f01", size = 9023543, upload-time = "2025-10-29T19:39:52.292Z" },
{ url = "https://files.pythonhosted.org/packages/b4/b8/72bcefb4be32e5a84f0b21de2552f16cdb4cae3eb271ac891c8199c26b1a/ty-0.0.1a25-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f13ea9815f4a54a0a303ca7bf411b0650e3c2a24fc6c7889ffba2c94f5e97a6a", size = 9700013, upload-time = "2025-10-29T19:39:57.283Z" },
{ url = "https://files.pythonhosted.org/packages/90/0d/cf7e794b840cf6b0bbecb022e593c543f85abad27a582241cf2095048cb1/ty-0.0.1a25-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eab6e33ebe202a71a50c3d5a5580e3bc1a85cda3ffcdc48cec3f1c693b7a873b", size = 9372574, upload-time = "2025-10-29T19:40:04.532Z" },
{ url = "https://files.pythonhosted.org/packages/1e/71/2d35e7d51b48eabd330e2f7b7e0bce541cbd95950c4d2f780e85f3366af1/ty-0.0.1a25-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6b9a31da43424cdab483703a54a561b93aabba84630788505329fc5294a9c62", size = 9535726, upload-time = "2025-10-29T19:40:06.548Z" },
{ url = "https://files.pythonhosted.org/packages/57/d3/01ecc23bbd8f3e0dfbcf9172d06d84e88155c5f416f1491137e8066fd859/ty-0.0.1a25-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a90d897a7c1a5ae9b41a4c7b0a42262a06361476ad88d783dbedd7913edadbc", size = 9003380, upload-time = "2025-10-29T19:40:08.683Z" },
{ url = "https://files.pythonhosted.org/packages/de/f9/cde9380d8a1a6ca61baeb9aecb12cbec90d489aa929be55cd78ad5c2ccd9/ty-0.0.1a25-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:93c7e7ab2859af0f866d34d27f4ae70dd4fb95b847387f082de1197f9f34e068", size = 8401833, upload-time = "2025-10-29T19:40:10.627Z" },
{ url = "https://files.pythonhosted.org/packages/0b/39/0acf3625b0c495011795a391016b572f97a812aca1d67f7a76621fdb9ebf/ty-0.0.1a25-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4a247061bd32bae3865a236d7f8b6c9916c80995db30ae1600999010f90623a9", size = 8706761, upload-time = "2025-10-29T19:40:12.575Z" },
{ url = "https://files.pythonhosted.org/packages/25/73/7de1648f3563dd9d416d36ab5f1649bfd7b47a179135027f31d44b89a246/ty-0.0.1a25-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1711dd587eccf04fd50c494dc39babe38f4cb345bc3901bf1d8149cac570e979", size = 8792426, upload-time = "2025-10-29T19:40:14.553Z" },
{ url = "https://files.pythonhosted.org/packages/7d/8a/b6e761a65eac7acd10b2e452f49b2d8ae0ea163ca36bb6b18b2dadae251b/ty-0.0.1a25-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5f4c9b0cf7995e2e3de9bab4d066063dea92019f2f62673b7574e3612643dd35", size = 9103991, upload-time = "2025-10-29T19:40:16.332Z" },
{ url = "https://files.pythonhosted.org/packages/e4/25/9324ae947fcc4322470326cf8276a3fc2f08dc82adec1de79d963fdf7af5/ty-0.0.1a25-py3-none-win32.whl", hash = "sha256:168fc8aee396d617451acc44cd28baffa47359777342836060c27aa6f37e2445", size = 8387095, upload-time = "2025-10-29T19:40:18.368Z" },
{ url = "https://files.pythonhosted.org/packages/3b/2b/cb12cbc7db1ba310aa7b1de9b4e018576f653105993736c086ee67d2ec02/ty-0.0.1a25-py3-none-win_amd64.whl", hash = "sha256:a2fad3d8e92bb4d57a8872a6f56b1aef54539d36f23ebb01abe88ac4338efafb", size = 9059225, upload-time = "2025-10-29T19:40:20.278Z" },
{ url = "https://files.pythonhosted.org/packages/2f/c1/f6be8cdd0bf387c1d8ee9d14bb299b7b5d2c0532f550a6693216a32ec0c5/ty-0.0.1a25-py3-none-win_arm64.whl", hash = "sha256:dde2962d448ed87c48736e9a4bb13715a4cced705525e732b1c0dac1d4c66e3d", size = 8536832, upload-time = "2025-10-29T19:40:22.014Z" },
]
[[package]] [[package]]
name = "types-pyyaml" name = "types-pyyaml"
version = "6.0.12.20250915" version = "6.0.12.20250915"