From 458ceb31b115f04852b243eba950c943ab27fa21 Mon Sep 17 00:00:00 2001 From: Emily Boudreaux Date: Sun, 8 Mar 2026 11:28:59 -0400 Subject: [PATCH] feat(main): initial commit --- .coverage | Bin 0 -> 53248 bytes .env.example | 4 + .gitignore | 10 + .python-version | 1 + Dockerfile | 26 + README.md | 0 api/__init__.py | 0 api/admin.py | 3 + api/api.py | 13 + api/apps.py | 5 + api/migrations/__init__.py | 0 api/models.py | 3 + api/routers/__init__.py | 0 api/routers/channel.py | 47 + api/routers/library.py | 38 + api/routers/schedule.py | 65 + api/routers/user.py | 52 + api/tests/__init__.py | 0 api/tests/test_channel.py | 67 + api/tests/test_library.py | 54 + api/tests/test_schedule.py | 106 + api/tests/test_user.py | 67 + api/views.py | 3 + core/__init__.py | 0 core/admin.py | 3 + core/apps.py | 5 + core/management/__init__.py | 0 core/management/commands/__init__.py | 0 core/management/commands/seed.py | 209 ++ core/management/commands/state.py | 60 + core/migrations/0001_initial.py | 1293 +++++++++ core/migrations/__init__.py | 0 core/models.py | 490 ++++ core/services/__init__.py | 0 core/services/scheduler.py | 108 + core/tests.py | 127 + core/views.py | 3 + db.sqlite3 | Bin 0 -> 561152 bytes docker-compose.yml | 30 + frontend/.gitignore | 24 + frontend/README.md | 16 + frontend/eslint.config.js | 29 + frontend/index.html | 13 + frontend/package-lock.json | 3210 ++++++++++++++++++++++ frontend/package.json | 29 + frontend/public/vite.svg | 1 + frontend/src/App.jsx | 33 + frontend/src/api.js | 24 + frontend/src/assets/react.svg | 1 + frontend/src/components/ChannelTuner.jsx | 134 + frontend/src/components/Guide.jsx | 70 + frontend/src/hooks/useRemoteControl.js | 67 + frontend/src/index.css | 284 ++ frontend/src/main.jsx | 10 + frontend/vite.config.js | 15 + main.py | 6 + manage.py | 22 + pyproject.toml | 18 + pytest.ini | 4 + pytv/__init__.py | 0 pytv/asgi.py | 16 + pytv/settings.py | 130 + pytv/urls.py | 25 + pytv/wsgi.py | 16 + schema.sql | 386 +++ uv.lock | 410 +++ 66 files changed, 7885 insertions(+) create mode 100644 .coverage create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .python-version create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 api/__init__.py create mode 100644 api/admin.py create mode 100644 api/api.py create mode 100644 api/apps.py create mode 100644 api/migrations/__init__.py create mode 100644 api/models.py create mode 100644 api/routers/__init__.py create mode 100644 api/routers/channel.py create mode 100644 api/routers/library.py create mode 100644 api/routers/schedule.py create mode 100644 api/routers/user.py create mode 100644 api/tests/__init__.py create mode 100644 api/tests/test_channel.py create mode 100644 api/tests/test_library.py create mode 100644 api/tests/test_schedule.py create mode 100644 api/tests/test_user.py create mode 100644 api/views.py create mode 100644 core/__init__.py create mode 100644 core/admin.py create mode 100644 core/apps.py create mode 100644 core/management/__init__.py create mode 100644 core/management/commands/__init__.py create mode 100644 core/management/commands/seed.py create mode 100644 core/management/commands/state.py create mode 100644 core/migrations/0001_initial.py create mode 100644 core/migrations/__init__.py create mode 100644 core/models.py create mode 100644 core/services/__init__.py create mode 100644 core/services/scheduler.py create mode 100644 core/tests.py create mode 100644 core/views.py create mode 100644 db.sqlite3 create mode 100644 docker-compose.yml create mode 100644 frontend/.gitignore create mode 100644 frontend/README.md create mode 100644 frontend/eslint.config.js create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/public/vite.svg create mode 100644 frontend/src/App.jsx create mode 100644 frontend/src/api.js create mode 100644 frontend/src/assets/react.svg create mode 100644 frontend/src/components/ChannelTuner.jsx create mode 100644 frontend/src/components/Guide.jsx create mode 100644 frontend/src/hooks/useRemoteControl.js create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.jsx create mode 100644 frontend/vite.config.js create mode 100644 main.py create mode 100755 manage.py create mode 100644 pyproject.toml create mode 100644 pytest.ini create mode 100644 pytv/__init__.py create mode 100644 pytv/asgi.py create mode 100644 pytv/settings.py create mode 100644 pytv/urls.py create mode 100644 pytv/wsgi.py create mode 100644 schema.sql create mode 100644 uv.lock diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..4ff2e6d252c300eac6498f84af238f1dda0d572e GIT binary patch literal 53248 zcmeI4U2GKB6@X`Uc4u~WXXo13V2r^YwW@$%Yz%}p3BfT5O`;%`G=wx#%QD^_+f#OT z%+9P~)qu4m6;&feQUB63Y2Ci0<)MF4r9MQ3s4oF2mHQSowET!vPMauDxddFm+jH;C zc*kI)S?3{%bw;~;=jWbt&-w1T_s*T!nI|6KX*z~BU={Paqiy0EIG*Po&@_(YWca4w z>n{?lMEwm&`M~;ot1{Ph#VobHzo-{7Lx$a{=`$W?EU@#C<9b`P^4*T5<;((e zvtXN!X%#f%kdY}lMs`jIJ1I1O0eC@gK-WI%&^}kvD=R>!COn6vm)#Q{0OJalkF_R9 ziw0q4bD@;)2jt%@<%enIfP`DHW6dWypR8kf&W5g=IqI_J@2u+dk<%J=rAh_!K*^Y? z{7$VV*~#1}Se+Vm0wUlGNwMXgCZ2<+G!EGZa&YRD)=Q4Xw$o7g=^pq|o@F zu>&knD4uk`>DV(X@~ohl**#C_(Bt#R4Q(Z}gM_xCTu!qFSd*dBnp-&#&A~R)?ZeQE zw4rI1mk})eM5B!w3pQx|Ijetmtg(o(4QL?H7x>u*N`(wzT&nB+Aq;>Y?c#rPpt{u zG7y9}e^_^hwA*c`yTS0)F|p;orSln1O-T39U-7wy2s8OR0;8XYPM>~-StipxHIpAR z%Iin;9P}o`x>9A$r>+jW859v1Th3wT@zh&hNz2J@wAn~XTWtz6eY`A??>aFRp%T&kU$ZBSj zKe*6tem68azkR_x9IehdPixGt^e5K^IV7GHh+i)h3*L`2T&B(0C zy5(Rk$i4!-G@K#@gNp(WFr=Gq3A9IatG(Yv=jZZHkP>U@1fM6@Ir!j$1dsp{Kmter z2_OL^fCP{L5EG5RFCxNB{{S0VIF~ zkN^@u0!RP}AOR$R1ZD^*!Ybas3=l~OZL)V2fZqRakDuenDe?pI4RV+IU-hK=U3HJz znmUtuDfL7ushm=Nr0h|aC*Mi_B$-a$mAI1lRU(~OCBGxTCTHc(fiNye00|%gB!C2v z022805a?@D_y}^D8N z!7qCvGh2cOPKV16tO6CW`c-6x^g_YNQ4K2>rXgp-OCyS9(6F&h;P!_!VZ<~J+91=n zBBbl^h69koeopc&BJiHt2!l_CN(~AObfS(sn=x z-u9_bV%UbBH=*&fS4MOP3jJ~lR5XSt)s5Afjtv_&^staOb=uswE1*YSfS!OY+mm(a zsI{Abj;)FCRT${{yzskE7P!M9oTzo2fuc9Y!}nKu7J6a8wn?g^ea#4o;e@D_c`Iw= zXwSGgCU8~=*W7ycrFxI(VuvJpR@`n&u~a$0?!{4n+7#PP(w zl#<%5UP-jeXOim@AANd;d$a@zAOR$R1dsp{Kmr`MJz9~%PND)Q!GU>HdtbB?TY!q% zTx!(n)I!lLrX?2-jY z*W$HtA+`r9sG&y~3~M`}gQ*q?8|`c+Sev^C3RNGDRvN>U>c(nK$1U^!)PvEAybwJB zTa@~A%sKy0)=`CluFsqQC$;6n$yj!rs zxaQWgFBRJ;m|>)7QQo9Hx`CjRSBf4q2BJ#^yY^z?ln{dD}&x$4^o zuHM^z?b4OA7tg+M_2%o}K0G#hzWT!To0r~y<@(K4m#@D6+e;sutBk%mdO{GJqP(ms zd}YlNPe}(C-rg6bSEZM$Q`&fSYjtvJ>crOZ$;rtZ6TN3|j0^7`t4@*04b{t2 zV^=TtRxgZIr}SyqIQGo88{^|0(-Y%kqth2gr@6_K)$+Bmv18*CqetM7^ltUSWG^XJ zCnrdCQX9QCM&9js>)ELZ$bRF(=(ZQHkM(v;PmNUx?3jY@GuOtd6XVCK+EFO;{9 zPn4&B^USs}No=GhCwZ}frjd;zp{W#!sZm*0c{K%_?THVMOixe$?CN1vROn%uSCWvf zXg+fD`q_W{ts|9yt(8qDrmvoRi+e+qW!P+P+ z|Kh1n^aKeY0VIF~kN^@u0!RP}AOR$R1dza71n~a 0 + from core.models import Airing + assert Airing.objects.filter(channel=channel).count() == data["airings_created"] diff --git a/api/tests/test_user.py b/api/tests/test_user.py new file mode 100644 index 0000000..b5dd08e --- /dev/null +++ b/api/tests/test_user.py @@ -0,0 +1,67 @@ +import pytest +from django.test import Client +from core.models import AppUser + +@pytest.fixture +def api_client(): + return Client() + +@pytest.fixture +def test_user(): + return AppUser.objects.create_user( + username="test_api_user", + email="test@api.tv", + password="testpassword123" + ) + +@pytest.mark.django_db +def test_list_users(api_client, test_user): + response = api_client.get("/api/user/") + assert response.status_code == 200 + data = response.json() + assert len(data) >= 1 + assert any(u["username"] == "test_api_user" for u in data) + +@pytest.mark.django_db +def test_get_user(api_client, test_user): + response = api_client.get(f"/api/user/{test_user.id}") + assert response.status_code == 200 + data = response.json() + assert data["username"] == "test_api_user" + assert "password" not in data # Ensure we don't leak passwords + +@pytest.mark.django_db +def test_create_user(api_client): + payload = { + "username": "new_viewer", + "email": "viewer@api.tv", + "password": "securepassword", + "is_superuser": False + } + response = api_client.post("/api/user/", data=payload, content_type="application/json") + assert response.status_code == 201 + data = response.json() + assert data["username"] == "new_viewer" + + # Verify DB + user = AppUser.objects.get(id=data["id"]) + assert user.username == "new_viewer" + assert not user.is_superuser + assert user.check_password("securepassword") + +@pytest.mark.django_db +def test_update_user(api_client, test_user): + payload = { + "email": "updated@api.tv", + "is_active": False + } + response = api_client.patch(f"/api/user/{test_user.id}", data=payload, content_type="application/json") + assert response.status_code == 200 + data = response.json() + assert data["email"] == "updated@api.tv" + assert data["is_active"] is False + + # Verify DB + test_user.refresh_from_db() + assert test_user.email == "updated@api.tv" + assert not test_user.is_active diff --git a/api/views.py b/api/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/api/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/admin.py b/core/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/core/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/core/apps.py b/core/apps.py new file mode 100644 index 0000000..5ef1d60 --- /dev/null +++ b/core/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CoreConfig(AppConfig): + name = "core" diff --git a/core/management/__init__.py b/core/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/management/commands/__init__.py b/core/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/management/commands/seed.py b/core/management/commands/seed.py new file mode 100644 index 0000000..a92be4b --- /dev/null +++ b/core/management/commands/seed.py @@ -0,0 +1,209 @@ +import random +from datetime import time, timedelta +from django.core.management.base import BaseCommand +from django.utils import timezone +from core.models import ( + AppUser, Library, Genre, ContentRating, MediaSource, + Series, MediaItem, Channel, ScheduleTemplate, ScheduleBlock, Airing +) + +class Command(BaseCommand): + help = 'Seeds the database with mock PYTV data including users, libraries, media, and channels.' + + def handle(self, *args, **kwargs): + self.stdout.write("Flushing existing data...") + # Since we cascade everything, deleting users usually drops almost everything + AppUser.objects.filter(username__startswith='mock_').delete() + Genre.objects.all().delete() + ContentRating.objects.all().delete() + + self.stdout.write("Seeding Users...") + admin_user = AppUser.objects.create_superuser('mock_admin', 'admin@pytv.local', 'admin') + viewer_user = AppUser.objects.create_user('mock_viewer', 'viewer@pytv.local', 'password') + + self.stdout.write("Seeding Genres & Ratings...") + g_action = Genre.objects.create(name="Action") + g_comedy = Genre.objects.create(name="Comedy") + g_drama = Genre.objects.create(name="Drama") + g_scifi = Genre.objects.create(name="Sci-Fi") + g_promo = Genre.objects.create(name="Promo/Bumper") + + r_pg = ContentRating.objects.create(system_name="TV Parental Guidelines", code="TV-PG", min_age=8) + r_14 = ContentRating.objects.create(system_name="TV Parental Guidelines", code="TV-14", min_age=14) + r_ma = ContentRating.objects.create(system_name="TV Parental Guidelines", code="TV-MA", min_age=17) + + self.stdout.write("Seeding Library & Media Sources...") + lib = Library.objects.create( + owner_user=admin_user, + name="Main Broadcasting Library", + visibility="public", + description="The core streaming library for PYTV mock." + ) + + source_movies = MediaSource.objects.create( + library=lib, + name="Mocked Local Movies", + source_type="local_directory", + uri="/mock/movies" + ) + + source_tv = MediaSource.objects.create( + library=lib, + name="Mocked Local TV Shows", + source_type="local_directory", + uri="/mock/tv" + ) + + source_bumpers = MediaSource.objects.create( + library=lib, + name="Network Bumpers", + source_type="local_directory", + uri="/mock/bumpers" + ) + + self.stdout.write("Seeding Series & Media Items...") + + # Movies + m1 = MediaItem.objects.create( + media_source=source_movies, + title="Space Rangers 3000", + item_kind="movie", + release_year=1999, + runtime_seconds=5400, # 1.5 hours + file_path="/mock/movies/space_rangers.mp4", + content_rating=r_pg + ) + m1.genres.add(g_action, g_scifi) + + m2 = MediaItem.objects.create( + media_source=source_movies, + title="The Laughing Policeman", + item_kind="movie", + release_year=2005, + runtime_seconds=6300, # 1.75 hours + file_path="/mock/movies/laughing_policeman.mp4", + content_rating=r_14 + ) + m2.genres.add(g_comedy, g_action) + + # Series + s1 = Series.objects.create(title="Neon City Nights", description="Cyberpunk detective drama.", release_year=2024) + + for ep in range(1, 6): + ep_item = MediaItem.objects.create( + media_source=source_tv, + series=s1, + title=f"Episode {ep}", + item_kind="episode", + season_number=1, + episode_number=ep, + runtime_seconds=2700, # 45 mins + file_path=f"/mock/tv/neon_city_nights/s01e0{ep}.mkv", + content_rating=r_ma + ) + ep_item.genres.add(g_drama, g_scifi) + + # Bumpers + for b in range(1, 4): + bump = MediaItem.objects.create( + media_source=source_bumpers, + title=f"Station Bumper {b}", + item_kind="bumper", + runtime_seconds=15, + file_path=f"/mock/bumpers/ident_{b}.mp4" + ) + bump.genres.add(g_promo) + + self.stdout.write("Seeding Channels and Scheduling Blocks...") + ch1 = Channel.objects.create( + owner_user=admin_user, + library=lib, + name="PYTV One", + slug="pytv-1", + channel_number=1, + description="The flagship generic network.", + scheduling_mode="template_driven", + visibility="public" + ) + + template = ScheduleTemplate.objects.create( + channel=ch1, + name="Standard Weekday", + timezone_name="UTC", + priority=10 + ) + + # Prime time block + b_prime = ScheduleBlock.objects.create( + schedule_template=template, + name="Prime Time Drama", + block_type="programming", + start_local_time=time(20, 0), + end_local_time=time(23, 0), + day_of_week_mask=127, # Everyday + default_genre=g_drama, + rotation_strategy="sequential", + pad_strategy="fill_with_interstitials" + ) + + ch2 = Channel.objects.create( + owner_user=admin_user, + library=lib, + name="Tears of Steel Channel", + slug="tears-of-steel", + channel_number=2, + description="All Sci-Fi all the time.", + scheduling_mode="template_driven", + visibility="public" + ) + + template2 = ScheduleTemplate.objects.create( + channel=ch2, + name="Sci-Fi Everyday", + timezone_name="UTC", + priority=10 + ) + + ScheduleBlock.objects.create( + schedule_template=template2, + name="Sci-Fi Block", + block_type="programming", + start_local_time=time(0, 0), + end_local_time=time(23, 59, 59), + day_of_week_mask=127, + default_genre=g_scifi, + rotation_strategy="random", + pad_strategy="none" + ) + + ch3 = Channel.objects.create( + owner_user=admin_user, + library=lib, + name="Sintel Classics", + slug="sintel-classics", + channel_number=3, + description="Classic movies and animation.", + scheduling_mode="template_driven", + visibility="public" + ) + + template3 = ScheduleTemplate.objects.create( + channel=ch3, + name="Comedy and Action", + timezone_name="UTC", + priority=10 + ) + + ScheduleBlock.objects.create( + schedule_template=template3, + name="Action Comedy", + block_type="programming", + start_local_time=time(0, 0), + end_local_time=time(23, 59, 59), + day_of_week_mask=127, + default_genre=g_action, + rotation_strategy="random", + pad_strategy="none" + ) + + self.stdout.write(self.style.SUCCESS("Successfully seeded the PYTV database with mock data.")) diff --git a/core/management/commands/state.py b/core/management/commands/state.py new file mode 100644 index 0000000..77343bc --- /dev/null +++ b/core/management/commands/state.py @@ -0,0 +1,60 @@ +from django.core.management.base import BaseCommand +from core.models import AppUser, Library, Channel, MediaItem, Airing, ScheduleTemplate +from django.utils import timezone +from datetime import timedelta + +class Command(BaseCommand): + help = "Displays a beautifully formatted terminal dashboard of the current backend state." + + def get_color(self, text, code): + """Helper to wrap string in bash color codes""" + return f"\033[{code}m{text}\033[0m" + + def handle(self, *args, **options): + # 1. Gather Aggregate Metrics + total_users = AppUser.objects.count() + total_libraries = Library.objects.count() + total_channels = Channel.objects.count() + total_media = MediaItem.objects.count() + total_templates = ScheduleTemplate.objects.count() + + # Gather near-term Airing metrics + now = timezone.now() + tomorrow = now + timedelta(days=1) + airings_today = Airing.objects.filter(starts_at__gte=now, starts_at__lt=tomorrow).count() + airings_total = Airing.objects.count() + + self.stdout.write(self.get_color("\n=== PYTV Backend State Dashboard ===", "1;34")) + self.stdout.write(f"\n{self.get_color('Core Metrics:', '1;36')}") + self.stdout.write(f" Users: {self.get_color(str(total_users), '1;32')}") + self.stdout.write(f" Libraries: {self.get_color(str(total_libraries), '1;32')}") + self.stdout.write(f" Media Items: {self.get_color(str(total_media), '1;32')}") + self.stdout.write(f" Channels: {self.get_color(str(total_channels), '1;32')}") + + self.stdout.write(f"\n{self.get_color('Scheduling Engine:', '1;36')}") + self.stdout.write(f" Active Templates: {self.get_color(str(total_templates), '1;33')}") + self.stdout.write(f" Airings (Next 24h): {self.get_color(str(airings_today), '1;33')}") + self.stdout.write(f" Airings (Total): {self.get_color(str(airings_total), '1;33')}") + + # 2. Build Tree View of Channels + self.stdout.write(f"\n{self.get_color('Channel Tree:', '1;36')}") + + channels = Channel.objects.prefetch_related('scheduletemplate_set').all() + if not channels: + self.stdout.write(" (No channels configured)") + + for c in channels: + status_color = "1;32" if c.is_active else "1;31" + status_text = "ACTIVE" if c.is_active else "INACTIVE" + self.stdout.write(f"\n 📺 [{c.channel_number or '-'}] {c.name} ({self.get_color(status_text, status_color)})") + + # Show templates + templates = c.scheduletemplate_set.filter(is_active=True).order_by('-priority') + if not templates: + self.stdout.write(" ⚠️ No active schedule templates.") + else: + for t in templates: + blocks_count = t.scheduleblock_set.count() + self.stdout.write(f" 📄 Template: {t.name} (Priority {t.priority}) -> {blocks_count} Blocks") + + self.stdout.write("\n") diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..252ebdf --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,1293 @@ +# Generated by Django 6.0.3 on 2026-03-08 14:36 + +import django.contrib.auth.models +import django.contrib.auth.validators +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ] + + operations = [ + migrations.CreateModel( + name="Airing", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("starts_at", models.DateTimeField(db_index=True)), + ("ends_at", models.DateTimeField(db_index=True)), + ( + "slot_kind", + models.CharField( + choices=[ + ("program", "Program"), + ("commercial", "Commercial"), + ("bumper", "Bumper"), + ("interstitial", "Interstitial"), + ("station_id", "Station ID"), + ], + max_length=24, + ), + ), + ( + "status", + models.CharField( + choices=[ + ("scheduled", "Scheduled"), + ("playing", "Playing"), + ("played", "Played"), + ("skipped", "Skipped"), + ("interrupted", "Interrupted"), + ("cancelled", "Cancelled"), + ], + db_index=True, + default="scheduled", + max_length=24, + ), + ), + ( + "source_reason", + models.CharField( + choices=[ + ("template", "Template"), + ("autofill", "Autofill"), + ("manual", "Manual"), + ("recovery", "Recovery"), + ], + max_length=24, + ), + ), + ("generation_batch_uuid", models.UUIDField()), + ("created_at", models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name="CommercialPolicy", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255, unique=True)), + ( + "mode", + models.CharField( + choices=[ + ("none", "None"), + ("replace_breaks", "Replace Breaks"), + ("fill_to_target", "Fill to Target"), + ("probabilistic", "Probabilistic"), + ], + max_length=24, + ), + ), + ("target_break_seconds", models.IntegerField(blank=True, null=True)), + ("max_break_seconds", models.IntegerField(blank=True, null=True)), + ("allow_same_ad_back_to_back", models.BooleanField(default=False)), + ("description", models.TextField(blank=True, null=True)), + ], + ), + migrations.CreateModel( + name="Series", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=255)), + ("description", models.TextField(blank=True, null=True)), + ("release_year", models.IntegerField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name="AppUser", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists." + }, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=150, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "email", + models.EmailField( + blank=True, max_length=254, verbose_name="email address" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.permission", + verbose_name="user permissions", + ), + ), + ], + options={ + "verbose_name": "user", + "verbose_name_plural": "users", + "abstract": False, + }, + managers=[ + ("objects", django.contrib.auth.models.UserManager()), + ], + ), + migrations.CreateModel( + name="AiringEvent", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "event_type", + models.CharField( + choices=[ + ("scheduled", "Scheduled"), + ("started", "Started"), + ("ended", "Ended"), + ("skipped", "Skipped"), + ("interrupted", "Interrupted"), + ("resumed", "Resumed"), + ("cancelled", "Cancelled"), + ], + max_length=24, + ), + ), + ("event_at", models.DateTimeField(auto_now_add=True)), + ("details_json", models.JSONField(default=dict)), + ( + "airing", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="core.airing" + ), + ), + ], + ), + migrations.CreateModel( + name="Channel", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255)), + ("slug", models.SlugField(max_length=64, unique=True)), + ("channel_number", models.IntegerField(blank=True, null=True)), + ("description", models.TextField(blank=True, null=True)), + ("timezone_name", models.CharField(default="UTC", max_length=64)), + ( + "visibility", + models.CharField( + choices=[ + ("private", "Private"), + ("shared", "Shared"), + ("public", "Public"), + ], + default="private", + max_length=16, + ), + ), + ( + "scheduling_mode", + models.CharField( + choices=[ + ("template_driven", "Template Driven"), + ("algorithmic", "Algorithmic"), + ("mixed", "Mixed"), + ], + default="template_driven", + max_length=24, + ), + ), + ("is_active", models.BooleanField(default=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "owner_user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.AddField( + model_name="airing", + name="channel", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="core.channel" + ), + ), + migrations.CreateModel( + name="ContentRating", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("system_name", models.CharField(max_length=64)), + ("code", models.CharField(max_length=32)), + ("description", models.TextField(blank=True, null=True)), + ("min_age", models.IntegerField(blank=True, null=True)), + ], + options={ + "constraints": [ + models.UniqueConstraint( + fields=("system_name", "code"), name="unique_content_rating" + ) + ], + }, + ), + migrations.CreateModel( + name="Genre", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=128, unique=True)), + ("description", models.TextField(blank=True, null=True)), + ( + "parent_genre", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="core.genre", + ), + ), + ], + ), + migrations.AddField( + model_name="channel", + name="default_genre", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="core.genre", + ), + ), + migrations.CreateModel( + name="Library", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255)), + ("description", models.TextField(blank=True, null=True)), + ( + "visibility", + models.CharField( + choices=[ + ("private", "Private"), + ("shared", "Shared"), + ("public", "Public"), + ], + default="private", + max_length=16, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "owner_user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.AddField( + model_name="channel", + name="library", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="core.library" + ), + ), + migrations.CreateModel( + name="LibraryMember", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "role", + models.CharField( + choices=[ + ("viewer", "Viewer"), + ("editor", "Editor"), + ("manager", "Manager"), + ], + max_length=16, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "library", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="core.library" + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.CreateModel( + name="MediaCollection", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255)), + ( + "collection_type", + models.CharField( + choices=[("manual", "Manual"), ("smart", "Smart")], + max_length=24, + ), + ), + ("definition_json", models.JSONField(default=dict)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "library", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="core.library" + ), + ), + ], + ), + migrations.CreateModel( + name="ChannelBranding", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("logo_path", models.TextField(blank=True, null=True)), + ("station_id_audio_path", models.TextField(blank=True, null=True)), + ("config_json", models.JSONField(default=dict)), + ( + "channel", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, to="core.channel" + ), + ), + ( + "commercial_policy", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="core.commercialpolicy", + ), + ), + ( + "bumper_collection", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="branded_channels_bumpers", + to="core.mediacollection", + ), + ), + ( + "fallback_fill_collection", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="branded_channels_fallback", + to="core.mediacollection", + ), + ), + ], + ), + migrations.CreateModel( + name="MediaItem", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=255)), + ("sort_title", models.CharField(blank=True, max_length=255, null=True)), + ("description", models.TextField(blank=True, null=True)), + ( + "item_kind", + models.CharField( + choices=[ + ("movie", "Movie"), + ("episode", "Episode"), + ("special", "Special"), + ("music_video", "Music Video"), + ("bumper", "Bumper"), + ("interstitial", "Interstitial"), + ("commercial", "Commercial"), + ], + db_index=True, + max_length=24, + ), + ), + ("season_number", models.IntegerField(blank=True, null=True)), + ("episode_number", models.IntegerField(blank=True, null=True)), + ("release_year", models.IntegerField(blank=True, null=True)), + ("runtime_seconds", models.PositiveIntegerField()), + ("file_path", models.TextField()), + ("file_hash", models.CharField(blank=True, max_length=128, null=True)), + ("thumbnail_path", models.TextField(blank=True, null=True)), + ( + "language_code", + models.CharField(blank=True, max_length=16, null=True), + ), + ("is_active", models.BooleanField(default=True)), + ("date_added_at", models.DateTimeField(auto_now_add=True)), + ("metadata_json", models.JSONField(default=dict)), + ( + "content_rating", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="core.contentrating", + ), + ), + ( + "genres", + models.ManyToManyField( + blank=True, related_name="media_items", to="core.genre" + ), + ), + ], + ), + migrations.CreateModel( + name="MediaCollectionItem", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("sort_order", models.IntegerField(default=0)), + ( + "media_collection", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="core.mediacollection", + ), + ), + ( + "media_item", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="core.mediaitem" + ), + ), + ], + ), + migrations.AddField( + model_name="mediacollection", + name="media_items", + field=models.ManyToManyField( + through="core.MediaCollectionItem", to="core.mediaitem" + ), + ), + migrations.AddField( + model_name="airing", + name="media_item", + field=models.ForeignKey( + on_delete=django.db.models.deletion.RESTRICT, to="core.mediaitem" + ), + ), + migrations.CreateModel( + name="MediaResumePoint", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("resume_seconds", models.PositiveIntegerField()), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "media_item", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="core.mediaitem" + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.CreateModel( + name="MediaSource", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255)), + ( + "source_type", + models.CharField( + choices=[ + ("local_directory", "Local Directory"), + ("network_share", "Network Share"), + ("manual_import", "Manual Import"), + ("playlist", "Playlist"), + ("stream", "Stream"), + ("api_feed", "API Feed"), + ], + max_length=32, + ), + ), + ("uri", models.TextField()), + ("recursive_scan", models.BooleanField(default=True)), + ("include_commercials", models.BooleanField(default=False)), + ("is_active", models.BooleanField(default=True)), + ("scan_interval_minutes", models.IntegerField(blank=True, null=True)), + ("last_scanned_at", models.DateTimeField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "library", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="core.library" + ), + ), + ], + ), + migrations.AddField( + model_name="mediaitem", + name="media_source", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="core.mediasource" + ), + ), + migrations.CreateModel( + name="ChannelSourceRule", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "rule_mode", + models.CharField( + choices=[ + ("allow", "Allow"), + ("prefer", "Prefer"), + ("avoid", "Avoid"), + ("block", "Block"), + ], + max_length=24, + ), + ), + ( + "weight", + models.DecimalField(decimal_places=4, default=1.0, max_digits=10), + ), + ("max_items_per_day", models.IntegerField(blank=True, null=True)), + ("max_runs_per_day", models.IntegerField(blank=True, null=True)), + ("min_repeat_gap_hours", models.IntegerField(blank=True, null=True)), + ("active_from", models.DateTimeField(blank=True, null=True)), + ("active_to", models.DateTimeField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "channel", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="core.channel" + ), + ), + ( + "media_collection", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.mediacollection", + ), + ), + ( + "media_source", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.mediasource", + ), + ), + ], + ), + migrations.CreateModel( + name="MediaSourceRule", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "rule_type", + models.CharField( + choices=[ + ("include_glob", "Include Glob"), + ("exclude_glob", "Exclude Glob"), + ("include_regex", "Include Regex"), + ("exclude_regex", "Exclude Regex"), + ], + max_length=24, + ), + ), + ("rule_value", models.TextField()), + ("sort_order", models.IntegerField(default=0)), + ( + "media_source", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="core.mediasource", + ), + ), + ], + ), + migrations.CreateModel( + name="ScheduleBlock", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255)), + ( + "block_type", + models.CharField( + choices=[ + ("programming", "Programming"), + ("commercial", "Commercial"), + ("filler", "Filler"), + ("off_air", "Off Air"), + ("special_event", "Special Event"), + ], + max_length=24, + ), + ), + ("start_local_time", models.TimeField()), + ("end_local_time", models.TimeField()), + ("day_of_week_mask", models.SmallIntegerField()), + ("spills_past_midnight", models.BooleanField(default=False)), + ( + "rotation_strategy", + models.CharField( + choices=[ + ("shuffle", "Shuffle"), + ("sequential", "Sequential"), + ("least_recent", "Least Recent"), + ("weighted_random", "Weighted Random"), + ], + default="shuffle", + max_length=24, + ), + ), + ( + "pad_strategy", + models.CharField( + choices=[ + ("hard_stop", "Hard Stop"), + ("truncate", "Truncate"), + ("fill_with_interstitials", "Fill With Interstitials"), + ("allow_overrun", "Allow Overrun"), + ], + default="fill_with_interstitials", + max_length=24, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "default_genre", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="core.genre", + ), + ), + ( + "max_content_rating", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="core.contentrating", + ), + ), + ( + "min_content_rating", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="core.contentrating", + ), + ), + ( + "preferred_collection", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="core.mediacollection", + ), + ), + ( + "preferred_source", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="core.mediasource", + ), + ), + ], + ), + migrations.CreateModel( + name="BlockSlot", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("slot_order", models.IntegerField()), + ( + "slot_kind", + models.CharField( + choices=[ + ("fixed_item", "Fixed Item"), + ("dynamic_pick", "Dynamic Pick"), + ("commercial_break", "Commercial Break"), + ("bumper", "Bumper"), + ("station_id", "Station ID"), + ], + max_length=24, + ), + ), + ( + "expected_duration_seconds", + models.IntegerField(blank=True, null=True), + ), + ("max_duration_seconds", models.IntegerField(blank=True, null=True)), + ("is_mandatory", models.BooleanField(default=True)), + ("selection_rule_json", models.JSONField(default=dict)), + ( + "media_collection", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="core.mediacollection", + ), + ), + ( + "media_item", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="core.mediaitem", + ), + ), + ( + "schedule_block", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="core.scheduleblock", + ), + ), + ], + ), + migrations.AddField( + model_name="airing", + name="schedule_block", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="core.scheduleblock", + ), + ), + migrations.CreateModel( + name="ScheduleTemplate", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255)), + ("description", models.TextField(blank=True, null=True)), + ("timezone_name", models.CharField(max_length=64)), + ("valid_from_date", models.DateField(blank=True, null=True)), + ("valid_to_date", models.DateField(blank=True, null=True)), + ("priority", models.IntegerField(default=0)), + ("is_active", models.BooleanField(default=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "channel", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="core.channel" + ), + ), + ], + ), + migrations.AddField( + model_name="scheduleblock", + name="schedule_template", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="core.scheduletemplate" + ), + ), + migrations.AddField( + model_name="airing", + name="schedule_template", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="core.scheduletemplate", + ), + ), + migrations.AddField( + model_name="mediaitem", + name="series", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="core.series", + ), + ), + migrations.CreateModel( + name="UserChannelState", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("is_favorite", models.BooleanField(default=False)), + ("last_tuned_at", models.DateTimeField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "channel", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="core.channel" + ), + ), + ( + "last_known_airing", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="core.airing", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.CreateModel( + name="WatchSession", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("started_at", models.DateTimeField(db_index=True)), + ("ended_at", models.DateTimeField(blank=True, null=True)), + ("position_seconds", models.PositiveIntegerField(default=0)), + ("client_id", models.CharField(blank=True, max_length=128, null=True)), + ("session_metadata", models.JSONField(default=dict)), + ( + "airing", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="core.airing", + ), + ), + ( + "channel", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="core.channel", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.CreateModel( + name="ChannelMember", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "role", + models.CharField( + choices=[ + ("viewer", "Viewer"), + ("editor", "Editor"), + ("manager", "Manager"), + ], + max_length=16, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "channel", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="core.channel" + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "constraints": [ + models.UniqueConstraint( + fields=("channel", "user"), name="unique_channel_member" + ) + ], + }, + ), + migrations.AddConstraint( + model_name="library", + constraint=models.UniqueConstraint( + fields=("owner_user", "name"), name="unique_library_name_per_user" + ), + ), + migrations.AddConstraint( + model_name="channel", + constraint=models.UniqueConstraint( + fields=("library", "name"), name="unique_channel_name_per_library" + ), + ), + migrations.AddConstraint( + model_name="channel", + constraint=models.UniqueConstraint( + fields=("library", "channel_number"), + name="unique_channel_number_per_library", + ), + ), + migrations.AddConstraint( + model_name="librarymember", + constraint=models.UniqueConstraint( + fields=("library", "user"), name="unique_library_member" + ), + ), + migrations.AddConstraint( + model_name="mediacollection", + constraint=models.UniqueConstraint( + fields=("library", "name"), + name="unique_media_collection_name_per_library", + ), + ), + migrations.AddConstraint( + model_name="mediaresumepoint", + constraint=models.UniqueConstraint( + fields=("user", "media_item"), name="unique_media_resume_point" + ), + ), + migrations.AddConstraint( + model_name="mediasource", + constraint=models.UniqueConstraint( + fields=("library", "name"), name="unique_media_source_name_per_library" + ), + ), + migrations.AddConstraint( + model_name="channelsourcerule", + constraint=models.CheckConstraint( + condition=models.Q( + models.Q( + ("media_collection__isnull", True), + ("media_source__isnull", False), + ), + models.Q( + ("media_collection__isnull", False), + ("media_source__isnull", True), + ), + _connector="OR", + ), + name="exactly_one_source_target_channel_source_rule", + ), + ), + migrations.AddConstraint( + model_name="blockslot", + constraint=models.UniqueConstraint( + fields=("schedule_block", "slot_order"), + name="unique_slot_order_per_block", + ), + ), + migrations.AddConstraint( + model_name="scheduletemplate", + constraint=models.UniqueConstraint( + fields=("channel", "name"), + name="unique_schedule_template_name_per_channel", + ), + ), + migrations.AddConstraint( + model_name="mediaitem", + constraint=models.UniqueConstraint( + fields=("media_source", "file_path"), + name="unique_media_item_path_per_source", + ), + ), + migrations.AddConstraint( + model_name="userchannelstate", + constraint=models.UniqueConstraint( + fields=("user", "channel"), name="unique_user_channel_state" + ), + ), + ] diff --git a/core/migrations/__init__.py b/core/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/models.py b/core/models.py new file mode 100644 index 0000000..22d18a2 --- /dev/null +++ b/core/models.py @@ -0,0 +1,490 @@ +from django.db import models +from django.contrib.auth.models import AbstractUser + +# ------------------------------------------------------------ +# users and access control +# ------------------------------------------------------------ + +class AppUser(AbstractUser): + # Using Django's built-in AbstractUser which covers username, email, password_hash (password), + # last_login, created_at (date_joined), is_admin (is_staff/is_superuser). + pass + +class Library(models.Model): + owner_user = models.ForeignKey(AppUser, on_delete=models.CASCADE) + name = models.CharField(max_length=255) + description = models.TextField(blank=True, null=True) + + class Visibility(models.TextChoices): + PRIVATE = 'private', 'Private' + SHARED = 'shared', 'Shared' + PUBLIC = 'public', 'Public' + + visibility = models.CharField( + max_length=16, + choices=Visibility.choices, + default=Visibility.PRIVATE + ) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + constraints = [ + models.UniqueConstraint(fields=['owner_user', 'name'], name='unique_library_name_per_user') + ] + + +class LibraryMember(models.Model): + library = models.ForeignKey(Library, on_delete=models.CASCADE) + user = models.ForeignKey(AppUser, on_delete=models.CASCADE) + + class Role(models.TextChoices): + VIEWER = 'viewer', 'Viewer' + EDITOR = 'editor', 'Editor' + MANAGER = 'manager', 'Manager' + + role = models.CharField(max_length=16, choices=Role.choices) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + constraints = [ + models.UniqueConstraint(fields=['library', 'user'], name='unique_library_member') + ] + + +# ------------------------------------------------------------ +# media metadata +# ------------------------------------------------------------ + +class Genre(models.Model): + parent_genre = models.ForeignKey('self', on_delete=models.SET_NULL, blank=True, null=True) + name = models.CharField(max_length=128, unique=True) + description = models.TextField(blank=True, null=True) + + +class ContentRating(models.Model): + system_name = models.CharField(max_length=64) + code = models.CharField(max_length=32) + description = models.TextField(blank=True, null=True) + min_age = models.IntegerField(blank=True, null=True) + + class Meta: + constraints = [ + models.UniqueConstraint(fields=['system_name', 'code'], name='unique_content_rating') + ] + + +class MediaSource(models.Model): + library = models.ForeignKey(Library, on_delete=models.CASCADE) + name = models.CharField(max_length=255) + + class SourceType(models.TextChoices): + LOCAL_DIRECTORY = 'local_directory', 'Local Directory' + NETWORK_SHARE = 'network_share', 'Network Share' + MANUAL_IMPORT = 'manual_import', 'Manual Import' + PLAYLIST = 'playlist', 'Playlist' + STREAM = 'stream', 'Stream' + API_FEED = 'api_feed', 'API Feed' + + source_type = models.CharField(max_length=32, choices=SourceType.choices) + uri = models.TextField() + recursive_scan = models.BooleanField(default=True) + include_commercials = models.BooleanField(default=False) + is_active = models.BooleanField(default=True) + scan_interval_minutes = models.IntegerField(blank=True, null=True) + last_scanned_at = models.DateTimeField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + constraints = [ + models.UniqueConstraint(fields=['library', 'name'], name='unique_media_source_name_per_library') + ] + + +class MediaSourceRule(models.Model): + media_source = models.ForeignKey(MediaSource, on_delete=models.CASCADE) + + class RuleType(models.TextChoices): + INCLUDE_GLOB = 'include_glob', 'Include Glob' + EXCLUDE_GLOB = 'exclude_glob', 'Exclude Glob' + INCLUDE_REGEX = 'include_regex', 'Include Regex' + EXCLUDE_REGEX = 'exclude_regex', 'Exclude Regex' + + rule_type = models.CharField(max_length=24, choices=RuleType.choices) + rule_value = models.TextField() + sort_order = models.IntegerField(default=0) + + +class Series(models.Model): + title = models.CharField(max_length=255) + description = models.TextField(blank=True, null=True) + release_year = models.IntegerField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + + +class MediaItem(models.Model): + media_source = models.ForeignKey(MediaSource, on_delete=models.CASCADE) + series = models.ForeignKey(Series, on_delete=models.SET_NULL, blank=True, null=True) + title = models.CharField(max_length=255) + sort_title = models.CharField(max_length=255, blank=True, null=True) + description = models.TextField(blank=True, null=True) + + class ItemKind(models.TextChoices): + MOVIE = 'movie', 'Movie' + EPISODE = 'episode', 'Episode' + SPECIAL = 'special', 'Special' + MUSIC_VIDEO = 'music_video', 'Music Video' + BUMPER = 'bumper', 'Bumper' + INTERSTITIAL = 'interstitial', 'Interstitial' + COMMERCIAL = 'commercial', 'Commercial' + + item_kind = models.CharField(max_length=24, choices=ItemKind.choices, db_index=True) + season_number = models.IntegerField(blank=True, null=True) + episode_number = models.IntegerField(blank=True, null=True) + release_year = models.IntegerField(blank=True, null=True) + runtime_seconds = models.PositiveIntegerField() + file_path = models.TextField() + file_hash = models.CharField(max_length=128, blank=True, null=True) + thumbnail_path = models.TextField(blank=True, null=True) + language_code = models.CharField(max_length=16, blank=True, null=True) + content_rating = models.ForeignKey(ContentRating, on_delete=models.SET_NULL, blank=True, null=True) + is_active = models.BooleanField(default=True) + date_added_at = models.DateTimeField(auto_now_add=True) + metadata_json = models.JSONField(default=dict) + + genres = models.ManyToManyField(Genre, related_name="media_items", blank=True) + + class Meta: + constraints = [ + models.UniqueConstraint(fields=['media_source', 'file_path'], name='unique_media_item_path_per_source') + ] + + +class MediaCollection(models.Model): + library = models.ForeignKey(Library, on_delete=models.CASCADE) + name = models.CharField(max_length=255) + + class CollectionType(models.TextChoices): + MANUAL = 'manual', 'Manual' + SMART = 'smart', 'Smart' + + collection_type = models.CharField(max_length=24, choices=CollectionType.choices) + definition_json = models.JSONField(default=dict) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + media_items = models.ManyToManyField(MediaItem, through='MediaCollectionItem') + + class Meta: + constraints = [ + models.UniqueConstraint(fields=['library', 'name'], name='unique_media_collection_name_per_library') + ] + + +class MediaCollectionItem(models.Model): + media_collection = models.ForeignKey(MediaCollection, on_delete=models.CASCADE) + media_item = models.ForeignKey(MediaItem, on_delete=models.CASCADE) + sort_order = models.IntegerField(default=0) + + +# ------------------------------------------------------------ +# channels and programming policy +# ------------------------------------------------------------ + +class Channel(models.Model): + owner_user = models.ForeignKey(AppUser, on_delete=models.CASCADE) + library = models.ForeignKey(Library, on_delete=models.CASCADE) + default_genre = models.ForeignKey(Genre, on_delete=models.SET_NULL, blank=True, null=True) + name = models.CharField(max_length=255) + slug = models.SlugField(max_length=64, unique=True) + channel_number = models.IntegerField(blank=True, null=True) + description = models.TextField(blank=True, null=True) + timezone_name = models.CharField(max_length=64, default='UTC') + + class Visibility(models.TextChoices): + PRIVATE = 'private', 'Private' + SHARED = 'shared', 'Shared' + PUBLIC = 'public', 'Public' + + visibility = models.CharField(max_length=16, choices=Visibility.choices, default=Visibility.PRIVATE) + + class SchedulingMode(models.TextChoices): + TEMPLATE_DRIVEN = 'template_driven', 'Template Driven' + ALGORITHMIC = 'algorithmic', 'Algorithmic' + MIXED = 'mixed', 'Mixed' + + scheduling_mode = models.CharField(max_length=24, choices=SchedulingMode.choices, default=SchedulingMode.TEMPLATE_DRIVEN) + is_active = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + constraints = [ + models.UniqueConstraint(fields=['library', 'name'], name='unique_channel_name_per_library'), + models.UniqueConstraint(fields=['library', 'channel_number'], name='unique_channel_number_per_library') + ] + + +class ChannelMember(models.Model): + channel = models.ForeignKey(Channel, on_delete=models.CASCADE) + user = models.ForeignKey(AppUser, on_delete=models.CASCADE) + + class Role(models.TextChoices): + VIEWER = 'viewer', 'Viewer' + EDITOR = 'editor', 'Editor' + MANAGER = 'manager', 'Manager' + + role = models.CharField(max_length=16, choices=Role.choices) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + constraints = [ + models.UniqueConstraint(fields=['channel', 'user'], name='unique_channel_member') + ] + + +class ChannelSourceRule(models.Model): + channel = models.ForeignKey(Channel, on_delete=models.CASCADE) + media_source = models.ForeignKey(MediaSource, on_delete=models.CASCADE, blank=True, null=True) + media_collection = models.ForeignKey(MediaCollection, on_delete=models.CASCADE, blank=True, null=True) + + class RuleMode(models.TextChoices): + ALLOW = 'allow', 'Allow' + PREFER = 'prefer', 'Prefer' + AVOID = 'avoid', 'Avoid' + BLOCK = 'block', 'Block' + + rule_mode = models.CharField(max_length=24, choices=RuleMode.choices) + weight = models.DecimalField(max_digits=10, decimal_places=4, default=1.0) + max_items_per_day = models.IntegerField(blank=True, null=True) + max_runs_per_day = models.IntegerField(blank=True, null=True) + min_repeat_gap_hours = models.IntegerField(blank=True, null=True) + active_from = models.DateTimeField(blank=True, null=True) + active_to = models.DateTimeField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + constraints = [ + models.CheckConstraint( + condition=( + models.Q(media_source__isnull=False, media_collection__isnull=True) | + models.Q(media_source__isnull=True, media_collection__isnull=False) + ), + name='exactly_one_source_target_channel_source_rule' + ) + ] + + +class CommercialPolicy(models.Model): + name = models.CharField(max_length=255, unique=True) + + class Mode(models.TextChoices): + NONE = 'none', 'None' + REPLACE_BREAKS = 'replace_breaks', 'Replace Breaks' + FILL_TO_TARGET = 'fill_to_target', 'Fill to Target' + PROBABILISTIC = 'probabilistic', 'Probabilistic' + + mode = models.CharField(max_length=24, choices=Mode.choices) + target_break_seconds = models.IntegerField(blank=True, null=True) + max_break_seconds = models.IntegerField(blank=True, null=True) + allow_same_ad_back_to_back = models.BooleanField(default=False) + description = models.TextField(blank=True, null=True) + + +class ChannelBranding(models.Model): + channel = models.OneToOneField(Channel, on_delete=models.CASCADE) + commercial_policy = models.ForeignKey(CommercialPolicy, on_delete=models.SET_NULL, blank=True, null=True) + logo_path = models.TextField(blank=True, null=True) + bumper_collection = models.ForeignKey(MediaCollection, on_delete=models.SET_NULL, blank=True, null=True, related_name='branded_channels_bumpers') + fallback_fill_collection = models.ForeignKey(MediaCollection, on_delete=models.SET_NULL, blank=True, null=True, related_name='branded_channels_fallback') + station_id_audio_path = models.TextField(blank=True, null=True) + config_json = models.JSONField(default=dict) + + +# ------------------------------------------------------------ +# schedule templates: recurring editorial structure +# ------------------------------------------------------------ + +class ScheduleTemplate(models.Model): + channel = models.ForeignKey(Channel, on_delete=models.CASCADE) + name = models.CharField(max_length=255) + description = models.TextField(blank=True, null=True) + timezone_name = models.CharField(max_length=64) + valid_from_date = models.DateField(blank=True, null=True) + valid_to_date = models.DateField(blank=True, null=True) + priority = models.IntegerField(default=0) + is_active = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + constraints = [ + models.UniqueConstraint(fields=['channel', 'name'], name='unique_schedule_template_name_per_channel') + ] + + +class ScheduleBlock(models.Model): + schedule_template = models.ForeignKey(ScheduleTemplate, on_delete=models.CASCADE) + name = models.CharField(max_length=255) + + class BlockType(models.TextChoices): + PROGRAMMING = 'programming', 'Programming' + COMMERCIAL = 'commercial', 'Commercial' + FILLER = 'filler', 'Filler' + OFF_AIR = 'off_air', 'Off Air' + SPECIAL_EVENT = 'special_event', 'Special Event' + + block_type = models.CharField(max_length=24, choices=BlockType.choices) + start_local_time = models.TimeField() + end_local_time = models.TimeField() + day_of_week_mask = models.SmallIntegerField() # 1 to 127 + spills_past_midnight = models.BooleanField(default=False) + default_genre = models.ForeignKey(Genre, on_delete=models.SET_NULL, blank=True, null=True) + min_content_rating = models.ForeignKey(ContentRating, on_delete=models.SET_NULL, blank=True, null=True, related_name='+') + max_content_rating = models.ForeignKey(ContentRating, on_delete=models.SET_NULL, blank=True, null=True, related_name='+') + preferred_collection = models.ForeignKey(MediaCollection, on_delete=models.SET_NULL, blank=True, null=True) + preferred_source = models.ForeignKey(MediaSource, on_delete=models.SET_NULL, blank=True, null=True) + + class RotationStrategy(models.TextChoices): + SHUFFLE = 'shuffle', 'Shuffle' + SEQUENTIAL = 'sequential', 'Sequential' + LEAST_RECENT = 'least_recent', 'Least Recent' + WEIGHTED_RANDOM = 'weighted_random', 'Weighted Random' + + rotation_strategy = models.CharField(max_length=24, choices=RotationStrategy.choices, default=RotationStrategy.SHUFFLE) + + class PadStrategy(models.TextChoices): + HARD_STOP = 'hard_stop', 'Hard Stop' + TRUNCATE = 'truncate', 'Truncate' + FILL_WITH_INTERSTITIALS = 'fill_with_interstitials', 'Fill With Interstitials' + ALLOW_OVERRUN = 'allow_overrun', 'Allow Overrun' + + pad_strategy = models.CharField(max_length=24, choices=PadStrategy.choices, default=PadStrategy.FILL_WITH_INTERSTITIALS) + created_at = models.DateTimeField(auto_now_add=True) + + +class BlockSlot(models.Model): + schedule_block = models.ForeignKey(ScheduleBlock, on_delete=models.CASCADE) + slot_order = models.IntegerField() + + class SlotKind(models.TextChoices): + FIXED_ITEM = 'fixed_item', 'Fixed Item' + DYNAMIC_PICK = 'dynamic_pick', 'Dynamic Pick' + COMMERCIAL_BREAK = 'commercial_break', 'Commercial Break' + BUMPER = 'bumper', 'Bumper' + STATION_ID = 'station_id', 'Station ID' + + slot_kind = models.CharField(max_length=24, choices=SlotKind.choices) + media_item = models.ForeignKey(MediaItem, on_delete=models.SET_NULL, blank=True, null=True) + media_collection = models.ForeignKey(MediaCollection, on_delete=models.SET_NULL, blank=True, null=True) + expected_duration_seconds = models.IntegerField(blank=True, null=True) + max_duration_seconds = models.IntegerField(blank=True, null=True) + is_mandatory = models.BooleanField(default=True) + selection_rule_json = models.JSONField(default=dict) + + class Meta: + constraints = [ + models.UniqueConstraint(fields=['schedule_block', 'slot_order'], name='unique_slot_order_per_block') + ] + + +# ------------------------------------------------------------ +# concrete schedule: actual planned / generated airtimes +# ------------------------------------------------------------ + +class Airing(models.Model): + channel = models.ForeignKey(Channel, on_delete=models.CASCADE) + schedule_template = models.ForeignKey(ScheduleTemplate, on_delete=models.SET_NULL, blank=True, null=True) + schedule_block = models.ForeignKey(ScheduleBlock, on_delete=models.SET_NULL, blank=True, null=True) + media_item = models.ForeignKey(MediaItem, on_delete=models.RESTRICT) + starts_at = models.DateTimeField(db_index=True) + ends_at = models.DateTimeField(db_index=True) + + class SlotKind(models.TextChoices): + PROGRAM = 'program', 'Program' + COMMERCIAL = 'commercial', 'Commercial' + BUMPER = 'bumper', 'Bumper' + INTERSTITIAL = 'interstitial', 'Interstitial' + STATION_ID = 'station_id', 'Station ID' + + slot_kind = models.CharField(max_length=24, choices=SlotKind.choices) + + class Status(models.TextChoices): + SCHEDULED = 'scheduled', 'Scheduled' + PLAYING = 'playing', 'Playing' + PLAYED = 'played', 'Played' + SKIPPED = 'skipped', 'Skipped' + INTERRUPTED = 'interrupted', 'Interrupted' + CANCELLED = 'cancelled', 'Cancelled' + + status = models.CharField(max_length=24, choices=Status.choices, default=Status.SCHEDULED, db_index=True) + + class SourceReason(models.TextChoices): + TEMPLATE = 'template', 'Template' + AUTOFILL = 'autofill', 'Autofill' + MANUAL = 'manual', 'Manual' + RECOVERY = 'recovery', 'Recovery' + + source_reason = models.CharField(max_length=24, choices=SourceReason.choices) + generation_batch_uuid = models.UUIDField() + created_at = models.DateTimeField(auto_now_add=True) + + +class AiringEvent(models.Model): + airing = models.ForeignKey(Airing, on_delete=models.CASCADE) + + class EventType(models.TextChoices): + SCHEDULED = 'scheduled', 'Scheduled' + STARTED = 'started', 'Started' + ENDED = 'ended', 'Ended' + SKIPPED = 'skipped', 'Skipped' + INTERRUPTED = 'interrupted', 'Interrupted' + RESUMED = 'resumed', 'Resumed' + CANCELLED = 'cancelled', 'Cancelled' + + event_type = models.CharField(max_length=24, choices=EventType.choices) + event_at = models.DateTimeField(auto_now_add=True) + details_json = models.JSONField(default=dict) + + +# ------------------------------------------------------------ +# viewer state and history +# ------------------------------------------------------------ + +class UserChannelState(models.Model): + user = models.ForeignKey(AppUser, on_delete=models.CASCADE) + channel = models.ForeignKey(Channel, on_delete=models.CASCADE) + is_favorite = models.BooleanField(default=False) + last_tuned_at = models.DateTimeField(blank=True, null=True) + last_known_airing = models.ForeignKey(Airing, on_delete=models.SET_NULL, blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + constraints = [ + models.UniqueConstraint(fields=['user', 'channel'], name='unique_user_channel_state') + ] + + +class WatchSession(models.Model): + user = models.ForeignKey(AppUser, on_delete=models.CASCADE) + channel = models.ForeignKey(Channel, on_delete=models.SET_NULL, blank=True, null=True) + airing = models.ForeignKey(Airing, on_delete=models.SET_NULL, blank=True, null=True) + started_at = models.DateTimeField(db_index=True) + ended_at = models.DateTimeField(blank=True, null=True) + position_seconds = models.PositiveIntegerField(default=0) + client_id = models.CharField(max_length=128, blank=True, null=True) + session_metadata = models.JSONField(default=dict) + + +class MediaResumePoint(models.Model): + user = models.ForeignKey(AppUser, on_delete=models.CASCADE) + media_item = models.ForeignKey(MediaItem, on_delete=models.CASCADE) + resume_seconds = models.PositiveIntegerField() + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + constraints = [ + models.UniqueConstraint(fields=['user', 'media_item'], name='unique_media_resume_point') + ] diff --git a/core/services/__init__.py b/core/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/services/scheduler.py b/core/services/scheduler.py new file mode 100644 index 0000000..72b040d --- /dev/null +++ b/core/services/scheduler.py @@ -0,0 +1,108 @@ +from datetime import datetime, timedelta, date, time, timezone +from core.models import Channel, ScheduleTemplate, ScheduleBlock, Airing, MediaItem +import random + +class ScheduleGenerator: + """ + A service that reads the latest ScheduleTemplate and Blocks for a given channel + and generates concrete Airings logic based on available matching MediaItems. + """ + + def __init__(self, channel: Channel): + self.channel = channel + + def generate_for_date(self, target_date: date) -> int: + """ + Idempotent generation of airings for a specific date on this channel. + Returns the number of new Airings created. + """ + # 1. Get the highest priority active template valid on this date + template = ScheduleTemplate.objects.filter( + channel=self.channel, + is_active=True + ).filter( + # Start date is null or <= target_date + valid_from_date__isnull=True + ).order_by('-priority').first() + + # In a real app we'd construct complex Q objects for the valid dates, + # but for PYTV mock we will just grab the highest priority active template. + if not template: + template = ScheduleTemplate.objects.filter(channel=self.channel, is_active=True).order_by('-priority').first() + if not template: + return 0 + + # 2. Extract day of week mask + # Python weekday: 0=Monday, 6=Sunday + # Our mask: bit 0 = Monday, bit 6 = Sunday + target_weekday_bit = 1 << target_date.weekday() + + blocks = template.scheduleblock_set.all() + airings_created = 0 + + for block in blocks: + # Check if block runs on this day + if not (block.day_of_week_mask & target_weekday_bit): + continue + + # Naive time combining mapping local time to UTC timeline without specific tz logic for simplicity now + start_dt = datetime.combine(target_date, block.start_local_time, tzinfo=timezone.utc) + end_dt = datetime.combine(target_date, block.end_local_time, tzinfo=timezone.utc) + + # If the block wraps past midnight (e.g. 23:00 to 02:00) + if end_dt <= start_dt: + end_dt += timedelta(days=1) + + # Clear existing airings in this window to allow idempotency + Airing.objects.filter( + channel=self.channel, + starts_at__gte=start_dt, + starts_at__lt=end_dt + ).delete() + + # 3. Pull matching Media Items + # Simplistic matching: pull items from library matching the block's genre + items_query = MediaItem.objects.filter(media_source__library=self.channel.library) + if block.default_genre: + items_query = items_query.filter(genres=block.default_genre) + + available_items = list(items_query.exclude(item_kind="bumper")) + if not available_items: + continue + + # Shuffle randomly for basic scheduling variety + random.shuffle(available_items) + + # 4. Fill the block + current_cursor = start_dt + item_index = 0 + + while current_cursor < end_dt and item_index < len(available_items): + item = available_items[item_index] + duration = timedelta(seconds=item.runtime_seconds or 3600) + + # Check if this item fits + if current_cursor + duration > end_dt: + # Item doesn't strictly fit, but we'll squeeze it in and break if needed + # Real systems pad this out or trim the slot. + pass + + import uuid + Airing.objects.create( + channel=self.channel, + schedule_template=template, + schedule_block=block, + media_item=item, + starts_at=current_cursor, + ends_at=current_cursor + duration, + slot_kind="program", + status="scheduled", + source_reason="template", + generation_batch_uuid=uuid.uuid4() + ) + + current_cursor += duration + item_index += 1 + airings_created += 1 + + return airings_created diff --git a/core/tests.py b/core/tests.py new file mode 100644 index 0000000..8fa7ee6 --- /dev/null +++ b/core/tests.py @@ -0,0 +1,127 @@ +from django.test import TestCase +from django.db import IntegrityError, transaction +from core.models import ( + AppUser, Library, LibraryMember, Genre, ContentRating, MediaSource, + Series, MediaItem, Channel, ScheduleTemplate, ScheduleBlock, Airing +) +from datetime import time +from django.utils import timezone +from uuid import uuid4 + +class PytvCoreTests(TestCase): + def setUp(self): + self.user1 = AppUser.objects.create_user(username="testuser1", email="test1@example.com", password="password123") + self.user2 = AppUser.objects.create_user(username="testuser2", email="test2@example.com", password="password123") + + def test_user_and_library_creation(self): + # Create library + lib = Library.objects.create(owner_user=self.user1, name="My Movies") + self.assertEqual(lib.visibility, Library.Visibility.PRIVATE) + + # Test unique constraint: Cannot have two libraries with the same name for the same user + with transaction.atomic(): + with self.assertRaises(IntegrityError): + Library.objects.create(owner_user=self.user1, name="My Movies") + + # Different user can have the same library name + lib2 = Library.objects.create(owner_user=self.user2, name="My Movies") + self.assertEqual(lib2.name, "My Movies") + + def test_media_source_and_item(self): + lib = Library.objects.create(owner_user=self.user1, name="My Movies") + source = MediaSource.objects.create( + library=lib, + name="Local NAS", + source_type=MediaSource.SourceType.LOCAL_DIRECTORY, + uri="/mnt/nas/movies" + ) + + item = MediaItem.objects.create( + media_source=source, + title="Cool Movie", + item_kind=MediaItem.ItemKind.MOVIE, + runtime_seconds=7200, + file_path="/mnt/nas/movies/cool_movie.mp4" + ) + + self.assertEqual(item.runtime_seconds, 7200) + + # Test unique constraint (same media source, same path) + with transaction.atomic(): + with self.assertRaises(IntegrityError): + MediaItem.objects.create( + media_source=source, + title="Another Movie", + item_kind=MediaItem.ItemKind.MOVIE, + runtime_seconds=5000, + file_path="/mnt/nas/movies/cool_movie.mp4" + ) + + # Test M2M Genre works beautifully + genre1 = Genre.objects.create(name="Action") + genre2 = Genre.objects.create(name="Sci-Fi") + item.genres.add(genre1, genre2) + self.assertEqual(item.genres.count(), 2) + + def test_channel_and_scheduling(self): + lib = Library.objects.create(owner_user=self.user1, name="TV Network") + channel = Channel.objects.create( + owner_user=self.user1, + library=lib, + name="Channel 1", + slug="ch-1", + channel_number=1 + ) + + template = ScheduleTemplate.objects.create( + channel=channel, + name="Weekday Prime Time", + timezone_name="America/New_York" + ) + + block = ScheduleBlock.objects.create( + schedule_template=template, + name="8PM Movie", + block_type=ScheduleBlock.BlockType.PROGRAMMING, + start_local_time=time(20, 0), + end_local_time=time(22, 0), + day_of_week_mask=62 # Mon-Fri bitmask + ) + + self.assertEqual(block.pad_strategy, ScheduleBlock.PadStrategy.FILL_WITH_INTERSTITIALS) + + def test_airing_constraints(self): + lib = Library.objects.create(owner_user=self.user1, name="TV Network") + source = MediaSource.objects.create( + library=lib, + name="Local NAS", + source_type=MediaSource.SourceType.LOCAL_DIRECTORY, + uri="/mnt/nas/tv" + ) + item = MediaItem.objects.create( + media_source=source, + title="Promo", + item_kind=MediaItem.ItemKind.BUMPER, + runtime_seconds=15, + file_path="/mnt/nas/tv/promo.mp4" + ) + channel = Channel.objects.create( + owner_user=self.user1, + library=lib, + name="Promo Channel", + slug="promo-ch", + channel_number=99 + ) + + now = timezone.now() + airing = Airing.objects.create( + channel=channel, + media_item=item, + starts_at=now, + ends_at=now + timezone.timedelta(seconds=15), + slot_kind=Airing.SlotKind.BUMPER, + source_reason=Airing.SourceReason.MANUAL, + generation_batch_uuid=uuid4() + ) + + self.assertEqual(airing.status, Airing.Status.SCHEDULED) diff --git a/core/views.py b/core/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/core/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/db.sqlite3 b/db.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..864424f0f6f67013911d4fd31e08e49677189d81 GIT binary patch literal 561152 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 bcu0S;V*mN{wJ(vAe!49?9O{)l4sriK{b7T? literal 0 HcmV?d00001 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3b728a1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,30 @@ +version: "3.9" + +services: + db: + image: postgres:16 + environment: + POSTGRES_USER: pytv_user + POSTGRES_PASSWORD: pytv_password + POSTGRES_DB: pytv_db + ports: + - "5433:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + + web: + build: . + command: python manage.py runserver 0.0.0.0:8000 + volumes: + - .:/app + ports: + - "8000:8000" + environment: + - DEBUG=True + - SECRET_KEY=django-insecure-development-key-replace-in-production + - DATABASE_URL=postgres://pytv_user:pytv_password@db:5432/pytv_db + depends_on: + - db + +volumes: + postgres_data: diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..18bc70e --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,16 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..4fa125d --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,29 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{js,jsx}'], + extends: [ + js.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + rules: { + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..c20fbd3 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + frontend + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..f26c4e7 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3210 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "axios": "^1.13.6", + "lucide-react": "^0.577.0", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "vite": "^7.3.1" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz", + "integrity": "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001777", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", + "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.4.tgz", + "integrity": "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.577.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", + "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..372aa6e --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,29 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.13.6", + "lucide-react": "^0.577.0", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "vite": "^7.3.1" + } +} diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..eee0fdf --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,33 @@ +import React, { useState } from 'react'; +import ChannelTuner from './components/ChannelTuner'; +import Guide from './components/Guide'; + +function App() { + const [showGuide, setShowGuide] = useState(false); + + // We capture the global Escape from within Guide for closing + React.useEffect(() => { + if (!showGuide) return; + const handleClose = (e) => { + if (['Escape', 'Backspace', 'Enter'].includes(e.key)) { + setShowGuide(false); + } + }; + window.addEventListener('keydown', handleClose); + return () => window.removeEventListener('keydown', handleClose); + }, [showGuide]); + + return ( + <> + {/* + The ChannelTuner always remains mounted in the background + so we don't drop video connections while browsing the guide. + */} + setShowGuide(!showGuide)} /> + + {showGuide && setShowGuide(false)} onSelectChannel={(id) => { console.log("Tuning to", id); setShowGuide(false); }} />} + + ); +} + +export default App; diff --git a/frontend/src/api.js b/frontend/src/api.js new file mode 100644 index 0000000..93a8d49 --- /dev/null +++ b/frontend/src/api.js @@ -0,0 +1,24 @@ +import axios from 'axios'; + +// The base URL relies on the Vite proxy in development, +// and same-origin in production. +const apiClient = axios.create({ + baseURL: '/api', + headers: { + 'Content-Type': 'application/json', + }, +}); + +export const fetchChannels = async () => { + const response = await apiClient.get('/channel/'); + return response.data; +}; + +// If a channel is selected, we can load its upcoming airings +export const fetchScheduleGenerations = async (channelId) => { + // We can trigger an immediate generation for the day to ensure there's data + const response = await apiClient.post(`/schedule/generate/${channelId}`); + return response.data; +}; + +// Future logic can query specific lists of Airings here... diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/ChannelTuner.jsx b/frontend/src/components/ChannelTuner.jsx new file mode 100644 index 0000000..66531c8 --- /dev/null +++ b/frontend/src/components/ChannelTuner.jsx @@ -0,0 +1,134 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { useRemoteControl } from '../hooks/useRemoteControl'; +import { fetchChannels } from '../api'; + +const FALLBACK_VIDEOS = [ + 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4', + 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/TearsOfSteel.mp4', + 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4' +]; + +export default function ChannelTuner({ onOpenGuide }) { + const [channels, setChannels] = useState([]); + const [loading, setLoading] = useState(true); + const [currentIndex, setCurrentIndex] = useState(0); + const [showOSD, setShowOSD] = useState(true); + const osdTimerRef = useRef(null); + + // The 3 buffer indices + const getPrevIndex = (index) => (index - 1 + channels.length) % channels.length; + const getNextIndex = (index) => (index + 1) % channels.length; + + const prevIndex = getPrevIndex(currentIndex); + const nextIndex = getNextIndex(currentIndex); + + const triggerOSD = () => { + setShowOSD(true); + if (osdTimerRef.current) clearTimeout(osdTimerRef.current); + osdTimerRef.current = setTimeout(() => setShowOSD(false), 5000); + }; + + const wrapChannelUp = () => { + setCurrentIndex(getNextIndex); + triggerOSD(); + }; + + const wrapChannelDown = () => { + setCurrentIndex(getPrevIndex); + triggerOSD(); + }; + + useRemoteControl({ + onChannelUp: wrapChannelUp, + onChannelDown: wrapChannelDown, + onSelect: triggerOSD, + onBack: onOpenGuide // Often on TVs 'Menu' or 'Back' opens Guide/App list + }); + + // Fetch channels from Django API + useEffect(() => { + fetchChannels().then(data => { + // If db gives us channels, pad them with a fallback video stream based on index + const mapped = data.map((ch, idx) => ({ + ...ch, + file: FALLBACK_VIDEOS[idx % FALLBACK_VIDEOS.length] + })); + if (mapped.length === 0) { + // Fallback if db is completely empty + mapped.push({ id: 99, channel_number: '99', name: 'Default Local feed', file: FALLBACK_VIDEOS[0] }); + } + setChannels(mapped); + setLoading(false); + }).catch(err => { + console.error(err); + setChannels([{ id: 99, channel_number: '99', name: 'Error Offline', file: FALLBACK_VIDEOS[0] }]); + setLoading(false); + }); + }, []); + + // Initial OSD hide + useEffect(() => { + if (!loading) triggerOSD(); + return () => clearTimeout(osdTimerRef.current); + }, [loading]); + + if (loading) { + return
Connecting to PYTV Backend...
; + } + + return ( +
+ {/* + We map over all channels, but selectively apply 'playing' or 'buffering' + classes to only the surrounding 3 elements. The rest are completely unrendered + to save immense DOM and memory resources. + */} + {channels.map((chan, index) => { + const isCurrent = index === currentIndex; + const isPrev = index === prevIndex; + const isNext = index === nextIndex; + + // Only mount the node if it's one of the 3 active buffers + if (!isCurrent && !isPrev && !isNext) return null; + + let stateClass = 'buffering'; + if (isCurrent) stateClass = 'playing'; + + return ( +
+ ); +} diff --git a/frontend/src/components/Guide.jsx b/frontend/src/components/Guide.jsx new file mode 100644 index 0000000..9fdce61 --- /dev/null +++ b/frontend/src/components/Guide.jsx @@ -0,0 +1,70 @@ +import React, { useState, useEffect } from 'react'; +import { useRemoteControl } from '../hooks/useRemoteControl'; +import { fetchChannels } from '../api'; + +export default function Guide({ onClose, onSelectChannel }) { + const [channels, setChannels] = useState([]); + const [selectedIndex, setSelectedIndex] = useState(0); + + useRemoteControl({ + onUp: () => setSelectedIndex(prev => (prev - 1 + channels.length) % channels.length), + onDown: () => setSelectedIndex(prev => (prev + 1) % channels.length), + onSelect: () => onSelectChannel(channels[selectedIndex].id), + onBack: onClose + }); + + const [currentTime, setCurrentTime] = useState(new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })); + + useEffect(() => { + fetchChannels().then(data => { + // Map channels securely, providing a fallback block if properties are missing + const mapped = data.map(ch => ({ + ...ch, + currentlyPlaying: ch.currentlyPlaying || { title: 'Live Broadcast', time: 'Now Playing' } + })); + if (mapped.length > 0) { + setChannels(mapped); + } else { + setChannels([{id: 99, channel_number: '99', name: 'No Channels Found', currentlyPlaying: {title: 'Empty Database', time: '--'}}]); + } + }).catch(err => { + console.error(err); + setChannels([{id: 99, channel_number: '99', name: 'Network Error', currentlyPlaying: {title: 'Could not reach PyTV server', time: '--'}}]); + }); + }, []); + + useEffect(() => { + const timer = setInterval(() => { + setCurrentTime(new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })); + }, 60000); + return () => clearInterval(timer); + }, []); + + return ( +
+
+

PYTV Guide

+
{currentTime}
+
+ +
+ {channels.length === 0 ?

Loading TV Guide...

: + channels.map((chan, idx) => ( +
+
+ {chan.channel_number} +
+
+
{chan.name} - {chan.currentlyPlaying.title}
+
{chan.currentlyPlaying.time}
+
+
+ ))} +
+ +
+ Press Enter to tune to the selected channel. Press Escape to exit guide. +
+
+ ); +} diff --git a/frontend/src/hooks/useRemoteControl.js b/frontend/src/hooks/useRemoteControl.js new file mode 100644 index 0000000..91c5aae --- /dev/null +++ b/frontend/src/hooks/useRemoteControl.js @@ -0,0 +1,67 @@ +import { useEffect, useCallback } from 'react'; + +/** + * useRemoteControl acts as a universal keyboard listener mapped + * to D-Pad interactions typical of Set Top Box/TV remotes. + */ +export function useRemoteControl({ onUp, onDown, onLeft, onRight, onSelect, onBack, onChannelUp, onChannelDown }) { + const handleKeyDown = useCallback( + (e) => { + // Ignore key events if the user is typing in an input + if (['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName)) return; + + switch (e.key) { + case 'ArrowUp': + e.preventDefault(); + // Often ArrowUp maps to ChannelUp, but sometimes it scrolls menus + if (onUp) onUp(); + else if (onChannelUp) onChannelUp(); + break; + case 'ArrowDown': + e.preventDefault(); + if (onDown) onDown(); + else if (onChannelDown) onChannelDown(); + break; + case 'ArrowLeft': + e.preventDefault(); + if (onLeft) onLeft(); + break; + case 'ArrowRight': + e.preventDefault(); + if (onRight) onRight(); + break; + case 'Enter': + case ' ': + e.preventDefault(); + if (onSelect) onSelect(); + break; + case 'Escape': + case 'Backspace': + e.preventDefault(); + if (onBack) onBack(); + break; + case '=': + case '+': // Fallback for some remotes channel + + case 'PageUp': + e.preventDefault(); + if (onChannelUp) onChannelUp(); + break; + case '-': // Fallback + case 'PageDown': + e.preventDefault(); + if (onChannelDown) onChannelDown(); + break; + default: + break; + } + }, + [onUp, onDown, onLeft, onRight, onSelect, onBack, onChannelUp, onChannelDown] + ); + + useEffect(() => { + window.addEventListener('keydown', handleKeyDown); + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, [handleKeyDown]); +} diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..b777fe0 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,284 @@ +:root { + --pytv-bg: #050505; + --pytv-surface: rgba(20, 20, 25, 0.7); + --pytv-surface-hover: rgba(40, 40, 50, 0.85); + --pytv-accent: #00d4ff; + --pytv-accent-glow: rgba(0, 212, 255, 0.4); + --pytv-text: #f0f0f0; + --pytv-text-dim: #a0a0a5; + --pytv-glass-border: rgba(255, 255, 255, 0.1); + --pytv-ch-overlay: rgba(0, 0, 0, 0.4); + + --font-osd: 'Inter', system-ui, -apple-system, sans-serif; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + background-color: var(--pytv-bg); + color: var(--pytv-text); + font-family: var(--font-osd); + overflow: hidden; /* Prevent scrolling for a TV app */ + width: 100vw; + height: 100vh; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +#root { + width: 100%; + height: 100%; + position: relative; +} + +/* Glassmorphism utility */ +.glass-panel { + background: var(--pytv-surface); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid var(--pytv-glass-border); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + border-radius: 12px; +} + +/* -------------------------------------------------------------------------- */ +/* Channel Tuner Specifics */ +/* -------------------------------------------------------------------------- */ + +.tuner-container { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #000; +} + +.tuner-video { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; + transition: opacity 0.4s ease-in-out, transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1); + opacity: 0; + pointer-events: none; + z-index: 1; +} + +.tuner-video.playing { + opacity: 1; + z-index: 2; + transform: scale(1); +} + +.tuner-video.buffering { + /* Keep it totally hidden but playing in background */ + opacity: 0; + transform: scale(1.05); /* Slight scale to feel like a zoom-out when swapping */ + z-index: 1; +} + +/* -------------------------------------------------------------------------- */ +/* OSD (On Screen Display) */ +/* -------------------------------------------------------------------------- */ + +.osd-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 10; + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 3rem; + background: linear-gradient(to bottom, rgba(0,0,0,0.4) 0%, transparent 20%, transparent 80%, rgba(0,0,0,0.6) 100%); + transition: opacity 0.5s ease; +} + +.osd-overlay.hidden { + opacity: 0; +} + +.osd-top { + display: flex; + justify-content: flex-end; +} + +.osd-bottom { + display: flex; + justify-content: flex-start; + align-items: flex-end; +} + +.osd-channel-bug { + font-size: 4rem; + font-weight: 800; + color: #fff; + text-shadow: 0 4px 12px rgba(0,0,0,0.8); + display: flex; + align-items: baseline; + gap: 0.5rem; +} + +.osd-channel-bug .ch-num { + font-size: 6rem; + color: var(--pytv-accent); + text-shadow: 0 0 20px var(--pytv-accent-glow); +} + +.osd-info-box { + padding: 1.5rem 2rem; + max-width: 600px; + animation: slideUp 0.4s cubic-bezier(0.2, 0.8, 0.2, 1); +} + +.osd-title { + font-size: 2.5rem; + font-weight: 700; + margin-bottom: 0.5rem; + line-height: 1.1; + text-wrap: balance; +} + +.osd-meta { + font-size: 1.25rem; + color: var(--pytv-text-dim); + display: flex; + gap: 1rem; + align-items: center; +} + +.osd-badge { + background: rgba(255,255,255,0.15); + padding: 0.2rem 0.6rem; + border-radius: 4px; + font-size: 0.9rem; + font-weight: 600; + letter-spacing: 1px; +} + +/* -------------------------------------------------------------------------- */ +/* EPG Guide */ +/* -------------------------------------------------------------------------- */ + +.guide-container { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.8); + backdrop-filter: blur(24px); + -webkit-backdrop-filter: blur(24px); + z-index: 20; + display: flex; + flex-direction: column; + padding: 3rem; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s ease; +} + +.guide-container.open { + opacity: 1; + pointer-events: all; +} + +.guide-header { + display: flex; + justify-content: space-between; + align-items: flex-end; + margin-bottom: 2rem; + border-bottom: 1px solid var(--pytv-glass-border); + padding-bottom: 1rem; +} + +.guide-header h1 { + font-size: 2.5rem; + font-weight: 300; + letter-spacing: 2px; +} + +.guide-clock { + font-size: 2rem; + font-weight: 700; + color: var(--pytv-accent); +} + +.guide-grid { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.5rem; + overflow: hidden; +} + +.guide-row { + display: grid; + grid-template-columns: 120px 1fr; + height: 80px; + background: rgba(255,255,255,0.03); + border-radius: 8px; + border: 1px solid transparent; + transition: all 0.2s; +} + +.guide-row.active { + background: var(--pytv-surface-hover); + border-color: var(--pytv-accent); + box-shadow: 0 0 15px var(--pytv-accent-glow); + transform: scale(1.01); +} + +.guide-ch-col { + display: flex; + align-items: center; + justify-content: center; + font-size: 2rem; + font-weight: 800; + color: var(--pytv-text-dim); + border-right: 1px solid rgba(255,255,255,0.05); +} + +.guide-row.active .guide-ch-col { + color: var(--pytv-text); +} + +.guide-prog-col { + padding: 1rem 1.5rem; + display: flex; + flex-direction: column; + justify-content: center; +} + +.guide-prog-title { + font-size: 1.4rem; + font-weight: 600; + margin-bottom: 0.2rem; +} + +.guide-prog-time { + font-size: 1rem; + color: var(--pytv-text-dim); +} + +/* Animations */ +@keyframes slideUp { + from { + transform: translateY(20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 0000000..b9a1a6d --- /dev/null +++ b/frontend/src/main.jsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.jsx' + +createRoot(document.getElementById('root')).render( + + + , +) diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..286a5bc --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], + server: { + proxy: { + '/api': { + target: 'http://localhost:8000', + changeOrigin: true + } + } + } +}) diff --git a/main.py b/main.py new file mode 100644 index 0000000..f4c71c6 --- /dev/null +++ b/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from pytv!") + + +if __name__ == "__main__": + main() diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..829de98 --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pytv.settings") + 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?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ae9e120 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,18 @@ +[project] +name = "pytv" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "django>=6.0.3", + "django-cors-headers>=4.9.0", + "django-environ>=0.13.0", + "django-ninja>=1.5.3", + "gunicorn>=25.1.0", + "psycopg>=3.3.3", + "pytest>=9.0.2", + "pytest-cov>=7.0.0", + "pytest-django>=4.12.0", + "pytest-sugar>=1.1.1", +] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..9106320 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +DJANGO_SETTINGS_MODULE = pytv.settings +python_files = tests.py test_*.py *_tests.py +addopts = --cov=core --cov=api --cov-report=term-missing diff --git a/pytv/__init__.py b/pytv/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pytv/asgi.py b/pytv/asgi.py new file mode 100644 index 0000000..601b1c1 --- /dev/null +++ b/pytv/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for pytv project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pytv.settings") + +application = get_asgi_application() diff --git a/pytv/settings.py b/pytv/settings.py new file mode 100644 index 0000000..cbd3f6b --- /dev/null +++ b/pytv/settings.py @@ -0,0 +1,130 @@ +""" +Django settings for pytv project. + +Generated by 'django-admin startproject' using Django 6.0.3. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/6.0/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +import environ + +env = environ.Env( + # set casting, default value + DEBUG=(bool, False) +) + +# Reading .env file +environ.Env.read_env(BASE_DIR / '.env') + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = env('SECRET_KEY', default='django-insecure-development-key') + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = env('DEBUG') + +ALLOWED_HOSTS = ['localhost', '127.0.0.1', '0.0.0.0'] +CORS_ALLOW_ALL_ORIGINS = True # Allows Vite dev server to connect + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "corsheaders", + "core", + "api", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "corsheaders.middleware.CorsMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "pytv.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "pytv.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/6.0/ref/settings/#databases + +DATABASES = { + 'default': env.db(default=f"sqlite:///{BASE_DIR / 'db.sqlite3'}") +} + + +# Password validation +# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/6.0/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_TZ = True + +AUTH_USER_MODEL = 'core.AppUser' + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/6.0/howto/static-files/ + +STATIC_URL = "static/" diff --git a/pytv/urls.py b/pytv/urls.py new file mode 100644 index 0000000..14ad09f --- /dev/null +++ b/pytv/urls.py @@ -0,0 +1,25 @@ +""" +URL configuration for pytv project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/6.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" + +from django.contrib import admin +from django.urls import path +from api.api import api + +urlpatterns = [ + path("admin/", admin.site.urls), + path("api/", api.urls), +] diff --git a/pytv/wsgi.py b/pytv/wsgi.py new file mode 100644 index 0000000..3a66fd8 --- /dev/null +++ b/pytv/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for pytv project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pytv.settings") + +application = get_wsgi_application() diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..a1da8e2 --- /dev/null +++ b/schema.sql @@ -0,0 +1,386 @@ +CREATE EXTENSION IF NOT EXISTS pgcrypto; + +-- ------------------------------------------------------------ +-- users and access control +-- ------------------------------------------------------------ + +CREATE TABLE app_user ( + id BIGSERIAL PRIMARY KEY, + username VARCHAR(64) NOT NULL UNIQUE, + email VARCHAR(320) UNIQUE, + password_hash TEXT NOT NULL, + is_admin BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + last_login_at TIMESTAMPTZ +); + +CREATE TABLE library ( + id BIGSERIAL PRIMARY KEY, + owner_user_id BIGINT NOT NULL REFERENCES app_user(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + description TEXT, + visibility VARCHAR(16) NOT NULL DEFAULT 'private' + CHECK (visibility IN ('private', 'shared', 'public')), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (owner_user_id, name) +); + +CREATE TABLE library_member ( + id BIGSERIAL PRIMARY KEY, + library_id BIGINT NOT NULL REFERENCES library(id) ON DELETE CASCADE, + user_id BIGINT NOT NULL REFERENCES app_user(id) ON DELETE CASCADE, + role VARCHAR(16) NOT NULL + CHECK (role IN ('viewer', 'editor', 'manager')), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (library_id, user_id) +); + +-- ------------------------------------------------------------ +-- media metadata +-- ------------------------------------------------------------ + +CREATE TABLE genre ( + id BIGSERIAL PRIMARY KEY, + parent_genre_id BIGINT REFERENCES genre(id) ON DELETE SET NULL, + name VARCHAR(128) NOT NULL UNIQUE, + description TEXT +); + +CREATE TABLE content_rating ( + id BIGSERIAL PRIMARY KEY, + system_name VARCHAR(64) NOT NULL, + code VARCHAR(32) NOT NULL, + description TEXT, + min_age INTEGER, + UNIQUE (system_name, code) +); + +CREATE TABLE media_source ( + id BIGSERIAL PRIMARY KEY, + library_id BIGINT NOT NULL REFERENCES library(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + source_type VARCHAR(32) NOT NULL + CHECK (source_type IN ( + 'local_directory', + 'network_share', + 'manual_import', + 'playlist', + 'stream', + 'api_feed' + )), + uri TEXT NOT NULL, + recursive_scan BOOLEAN NOT NULL DEFAULT TRUE, + include_commercials BOOLEAN NOT NULL DEFAULT FALSE, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + scan_interval_minutes INTEGER, + last_scanned_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (library_id, name) +); + +CREATE TABLE media_source_rule ( + id BIGSERIAL PRIMARY KEY, + media_source_id BIGINT NOT NULL REFERENCES media_source(id) ON DELETE CASCADE, + rule_type VARCHAR(24) NOT NULL + CHECK (rule_type IN ('include_glob', 'exclude_glob', 'include_regex', 'exclude_regex')), + rule_value TEXT NOT NULL, + sort_order INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE series ( + id BIGSERIAL PRIMARY KEY, + title VARCHAR(255) NOT NULL, + description TEXT, + release_year INTEGER, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE media_item ( + id BIGSERIAL PRIMARY KEY, + media_source_id BIGINT NOT NULL REFERENCES media_source(id) ON DELETE CASCADE, + series_id BIGINT REFERENCES series(id) ON DELETE SET NULL, + title VARCHAR(255) NOT NULL, + sort_title VARCHAR(255), + description TEXT, + item_kind VARCHAR(24) NOT NULL + CHECK (item_kind IN ( + 'movie', + 'episode', + 'special', + 'music_video', + 'bumper', + 'interstitial', + 'commercial' + )), + season_number INTEGER, + episode_number INTEGER, + release_year INTEGER, + runtime_seconds INTEGER NOT NULL CHECK (runtime_seconds > 0), + file_path TEXT NOT NULL, + file_hash VARCHAR(128), + thumbnail_path TEXT, + language_code VARCHAR(16), + content_rating_id BIGINT REFERENCES content_rating(id) ON DELETE SET NULL, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + date_added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + metadata_json JSONB NOT NULL DEFAULT '{}'::JSONB, + UNIQUE (media_source_id, file_path) +); + +CREATE INDEX idx_media_item_series_id ON media_item(series_id); +CREATE INDEX idx_media_item_item_kind ON media_item(item_kind); +CREATE INDEX idx_media_item_rating_id ON media_item(content_rating_id); +CREATE INDEX idx_media_item_source_id ON media_item(media_source_id); + +CREATE TABLE media_item_genre ( + media_item_id BIGINT NOT NULL REFERENCES media_item(id) ON DELETE CASCADE, + genre_id BIGINT NOT NULL REFERENCES genre(id) ON DELETE CASCADE, + PRIMARY KEY (media_item_id, genre_id) +); + +CREATE TABLE media_collection ( + id BIGSERIAL PRIMARY KEY, + library_id BIGINT NOT NULL REFERENCES library(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + collection_type VARCHAR(24) NOT NULL + CHECK (collection_type IN ('manual', 'smart')), + definition_json JSONB NOT NULL DEFAULT '{}'::JSONB, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (library_id, name) +); + +CREATE TABLE media_collection_item ( + media_collection_id BIGINT NOT NULL REFERENCES media_collection(id) ON DELETE CASCADE, + media_item_id BIGINT NOT NULL REFERENCES media_item(id) ON DELETE CASCADE, + sort_order INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (media_collection_id, media_item_id) +); + +-- ------------------------------------------------------------ +-- channels and programming policy +-- ------------------------------------------------------------ + +CREATE TABLE channel ( + id BIGSERIAL PRIMARY KEY, + owner_user_id BIGINT NOT NULL REFERENCES app_user(id) ON DELETE CASCADE, + library_id BIGINT NOT NULL REFERENCES library(id) ON DELETE CASCADE, + default_genre_id BIGINT REFERENCES genre(id) ON DELETE SET NULL, + name VARCHAR(255) NOT NULL, + slug VARCHAR(64) NOT NULL UNIQUE, + channel_number INTEGER, + description TEXT, + timezone_name VARCHAR(64) NOT NULL DEFAULT 'UTC', + visibility VARCHAR(16) NOT NULL DEFAULT 'private' + CHECK (visibility IN ('private', 'shared', 'public')), + scheduling_mode VARCHAR(24) NOT NULL DEFAULT 'template_driven' + CHECK (scheduling_mode IN ('template_driven', 'algorithmic', 'mixed')), + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (library_id, name), + UNIQUE (library_id, channel_number) +); + +CREATE TABLE channel_member ( + id BIGSERIAL PRIMARY KEY, + channel_id BIGINT NOT NULL REFERENCES channel(id) ON DELETE CASCADE, + user_id BIGINT NOT NULL REFERENCES app_user(id) ON DELETE CASCADE, + role VARCHAR(16) NOT NULL + CHECK (role IN ('viewer', 'editor', 'manager')), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (channel_id, user_id) +); + +CREATE TABLE channel_source_rule ( + id BIGSERIAL PRIMARY KEY, + channel_id BIGINT NOT NULL REFERENCES channel(id) ON DELETE CASCADE, + media_source_id BIGINT REFERENCES media_source(id) ON DELETE CASCADE, + media_collection_id BIGINT REFERENCES media_collection(id) ON DELETE CASCADE, + rule_mode VARCHAR(24) NOT NULL + CHECK (rule_mode IN ('allow', 'prefer', 'avoid', 'block')), + weight NUMERIC(10,4) NOT NULL DEFAULT 1.0 CHECK (weight >= 0), + max_items_per_day INTEGER, + max_runs_per_day INTEGER, + min_repeat_gap_hours INTEGER, + active_from TIMESTAMPTZ, + active_to TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + CHECK ( + (media_source_id IS NOT NULL AND media_collection_id IS NULL) + OR + (media_source_id IS NULL AND media_collection_id IS NOT NULL) + ) +); + +CREATE TABLE commercial_policy ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL UNIQUE, + mode VARCHAR(24) NOT NULL + CHECK (mode IN ('none', 'replace_breaks', 'fill_to_target', 'probabilistic')), + target_break_seconds INTEGER, + max_break_seconds INTEGER, + allow_same_ad_back_to_back BOOLEAN NOT NULL DEFAULT FALSE, + description TEXT +); + +CREATE TABLE channel_branding ( + id BIGSERIAL PRIMARY KEY, + channel_id BIGINT NOT NULL UNIQUE REFERENCES channel(id) ON DELETE CASCADE, + commercial_policy_id BIGINT REFERENCES commercial_policy(id) ON DELETE SET NULL, + logo_path TEXT, + bumper_collection_id BIGINT REFERENCES media_collection(id) ON DELETE SET NULL, + fallback_fill_collection_id BIGINT REFERENCES media_collection(id) ON DELETE SET NULL, + station_id_audio_path TEXT, + config_json JSONB NOT NULL DEFAULT '{}'::JSONB +); + +-- ------------------------------------------------------------ +-- schedule templates: recurring editorial structure +-- ------------------------------------------------------------ + +CREATE TABLE schedule_template ( + id BIGSERIAL PRIMARY KEY, + channel_id BIGINT NOT NULL REFERENCES channel(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + description TEXT, + timezone_name VARCHAR(64) NOT NULL, + valid_from_date DATE, + valid_to_date DATE, + priority INTEGER NOT NULL DEFAULT 0, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (channel_id, name) +); + +CREATE TABLE schedule_block ( + id BIGSERIAL PRIMARY KEY, + schedule_template_id BIGINT NOT NULL REFERENCES schedule_template(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + block_type VARCHAR(24) NOT NULL + CHECK (block_type IN ('programming', 'commercial', 'filler', 'off_air', 'special_event')), + start_local_time TIME NOT NULL, + end_local_time TIME NOT NULL, + day_of_week_mask SMALLINT NOT NULL CHECK (day_of_week_mask BETWEEN 1 AND 127), + spills_past_midnight BOOLEAN NOT NULL DEFAULT FALSE, + default_genre_id BIGINT REFERENCES genre(id) ON DELETE SET NULL, + min_content_rating_id BIGINT REFERENCES content_rating(id) ON DELETE SET NULL, + max_content_rating_id BIGINT REFERENCES content_rating(id) ON DELETE SET NULL, + preferred_collection_id BIGINT REFERENCES media_collection(id) ON DELETE SET NULL, + preferred_source_id BIGINT REFERENCES media_source(id) ON DELETE SET NULL, + rotation_strategy VARCHAR(24) NOT NULL DEFAULT 'shuffle' + CHECK (rotation_strategy IN ('shuffle', 'sequential', 'least_recent', 'weighted_random')), + pad_strategy VARCHAR(24) NOT NULL DEFAULT 'fill_with_interstitials' + CHECK (pad_strategy IN ('hard_stop', 'truncate', 'fill_with_interstitials', 'allow_overrun')), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + CHECK ( + end_local_time > start_local_time OR spills_past_midnight = TRUE + ) +); + +CREATE TABLE block_slot ( + id BIGSERIAL PRIMARY KEY, + schedule_block_id BIGINT NOT NULL REFERENCES schedule_block(id) ON DELETE CASCADE, + slot_order INTEGER NOT NULL, + slot_kind VARCHAR(24) NOT NULL + CHECK (slot_kind IN ('fixed_item', 'dynamic_pick', 'commercial_break', 'bumper', 'station_id')), + media_item_id BIGINT REFERENCES media_item(id) ON DELETE SET NULL, + media_collection_id BIGINT REFERENCES media_collection(id) ON DELETE SET NULL, + expected_duration_seconds INTEGER, + max_duration_seconds INTEGER, + is_mandatory BOOLEAN NOT NULL DEFAULT TRUE, + selection_rule_json JSONB NOT NULL DEFAULT '{}'::JSONB, + UNIQUE (schedule_block_id, slot_order), + CHECK ( + slot_kind <> 'fixed_item' OR media_item_id IS NOT NULL + ) +); + +-- ------------------------------------------------------------ +-- concrete schedule: actual planned / generated airtimes +-- ------------------------------------------------------------ + +CREATE TABLE airing ( + id BIGSERIAL PRIMARY KEY, + channel_id BIGINT NOT NULL REFERENCES channel(id) ON DELETE CASCADE, + schedule_template_id BIGINT REFERENCES schedule_template(id) ON DELETE SET NULL, + schedule_block_id BIGINT REFERENCES schedule_block(id) ON DELETE SET NULL, + media_item_id BIGINT NOT NULL REFERENCES media_item(id) ON DELETE RESTRICT, + starts_at TIMESTAMPTZ NOT NULL, + ends_at TIMESTAMPTZ NOT NULL, + slot_kind VARCHAR(24) NOT NULL + CHECK (slot_kind IN ('program', 'commercial', 'bumper', 'interstitial', 'station_id')), + status VARCHAR(24) NOT NULL DEFAULT 'scheduled' + CHECK (status IN ('scheduled', 'playing', 'played', 'skipped', 'interrupted', 'cancelled')), + source_reason VARCHAR(24) NOT NULL + CHECK (source_reason IN ('template', 'autofill', 'manual', 'recovery')), + generation_batch_uuid UUID NOT NULL DEFAULT gen_random_uuid(), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + CHECK (ends_at > starts_at) +); + +CREATE INDEX idx_airing_channel_time ON airing(channel_id, starts_at, ends_at); +CREATE INDEX idx_airing_media_item_time ON airing(media_item_id, starts_at); +CREATE INDEX idx_airing_status ON airing(status); + +CREATE TABLE airing_event ( + id BIGSERIAL PRIMARY KEY, + airing_id BIGINT NOT NULL REFERENCES airing(id) ON DELETE CASCADE, + event_type VARCHAR(24) NOT NULL + CHECK (event_type IN ('scheduled', 'started', 'ended', 'skipped', 'interrupted', 'resumed', 'cancelled')), + event_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + details_json JSONB NOT NULL DEFAULT '{}'::JSONB +); + +-- ------------------------------------------------------------ +-- viewer state and history +-- ------------------------------------------------------------ + +CREATE TABLE user_channel_state ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES app_user(id) ON DELETE CASCADE, + channel_id BIGINT NOT NULL REFERENCES channel(id) ON DELETE CASCADE, + is_favorite BOOLEAN NOT NULL DEFAULT FALSE, + last_tuned_at TIMESTAMPTZ, + last_known_airing_id BIGINT REFERENCES airing(id) ON DELETE SET NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (user_id, channel_id) +); + +CREATE TABLE watch_session ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES app_user(id) ON DELETE CASCADE, + channel_id BIGINT REFERENCES channel(id) ON DELETE SET NULL, + airing_id BIGINT REFERENCES airing(id) ON DELETE SET NULL, + started_at TIMESTAMPTZ NOT NULL, + ended_at TIMESTAMPTZ, + position_seconds INTEGER NOT NULL DEFAULT 0 CHECK (position_seconds >= 0), + client_id VARCHAR(128), + session_metadata JSONB NOT NULL DEFAULT '{}'::JSONB, + CHECK (ended_at IS NULL OR ended_at >= started_at) +); + +CREATE TABLE media_resume_point ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES app_user(id) ON DELETE CASCADE, + media_item_id BIGINT NOT NULL REFERENCES media_item(id) ON DELETE CASCADE, + resume_seconds INTEGER NOT NULL CHECK (resume_seconds >= 0), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (user_id, media_item_id) +); + +-- ------------------------------------------------------------ +-- useful integrity helpers +-- ------------------------------------------------------------ + +CREATE INDEX idx_channel_source_rule_channel ON channel_source_rule(channel_id); +CREATE INDEX idx_schedule_template_channel ON schedule_template(channel_id, is_active); +CREATE INDEX idx_schedule_block_template ON schedule_block(schedule_template_id, day_of_week_mask); +CREATE INDEX idx_block_slot_block_order ON block_slot(schedule_block_id, slot_order); +CREATE INDEX idx_user_channel_state_user ON user_channel_state(user_id); +CREATE INDEX idx_watch_session_user_time ON watch_session(user_id, started_at); + diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..1fa27a8 --- /dev/null +++ b/uv.lock @@ -0,0 +1,410 @@ +version = 1 +revision = 2 +requires-python = ">=3.13" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "asgiref" +version = "3.11.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", size = 38550, upload-time = "2026-02-03T13:30:14.33Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" }, + { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" }, + { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" }, + { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" }, + { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" }, + { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" }, + { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" }, + { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" }, + { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" }, + { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" }, + { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" }, + { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" }, + { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, + { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, + { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, + { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, + { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, + { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, + { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, + { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, + { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, + { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, + { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, + { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, + { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, + { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, +] + +[[package]] +name = "django" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "sqlparse" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/e1/894115c6bd70e2c8b66b0c40a3c367d83a5a48c034a4d904d31b62f7c53a/django-6.0.3.tar.gz", hash = "sha256:90be765ee756af8a6cbd6693e56452404b5ad15294f4d5e40c0a55a0f4870fe1", size = 10872701, upload-time = "2026-03-03T13:55:15.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/b1/23f2556967c45e34d3d3cf032eb1bd3ef925ee458667fb99052a0b3ea3a6/django-6.0.3-py3-none-any.whl", hash = "sha256:2e5974441491ddb34c3f13d5e7a9f97b07ba03bf70234c0a9c68b79bbb235bc3", size = 8358527, upload-time = "2026-03-03T13:55:10.552Z" }, +] + +[[package]] +name = "django-cors-headers" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/39/55822b15b7ec87410f34cd16ce04065ff390e50f9e29f31d6d116fc80456/django_cors_headers-4.9.0.tar.gz", hash = "sha256:fe5d7cb59fdc2c8c646ce84b727ac2bca8912a247e6e68e1fb507372178e59e8", size = 21458, upload-time = "2025-09-18T10:40:52.326Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/d8/19ed1e47badf477d17fb177c1c19b5a21da0fd2d9f093f23be3fb86c5fab/django_cors_headers-4.9.0-py3-none-any.whl", hash = "sha256:15c7f20727f90044dcee2216a9fd7303741a864865f0c3657e28b7056f61b449", size = 12809, upload-time = "2025-09-18T10:40:50.843Z" }, +] + +[[package]] +name = "django-environ" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/3c/60983e6ec9b24a8d8588eecebfd21123cba980bce0a905807a27692f0860/django_environ-0.13.0.tar.gz", hash = "sha256:6c401e4c219442c2c4588c2116d5292b5484a6f69163ed09cd41f3943bfb645f", size = 63529, upload-time = "2026-02-18T01:08:08.791Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/00/3767393ece946084e1c6830a33ffb8e39d68642e27ad5ac7d4c8bd5de866/django_environ-0.13.0-py3-none-any.whl", hash = "sha256:37799d14cd78222c6fd8298e48bfe17965ff8e586091ad66a463e52e0e7b799e", size = 20682, upload-time = "2026-02-18T01:08:07.359Z" }, +] + +[[package]] +name = "django-ninja" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/84/27a5fceac29bd85eb8dc8a6697e93019a8742d626180f0d67b894e20a8a1/django_ninja-1.5.3.tar.gz", hash = "sha256:974803944965ad0566071633ffd4999a956f2ad1ecbed815c0de37c1c969592b", size = 3658996, upload-time = "2026-01-10T20:02:23.821Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/b3/30600696c2532fcf026259f2f4980b364cb6847518bb4b3365d42a4a3afe/django_ninja-1.5.3-py3-none-any.whl", hash = "sha256:0a6ead5b4e57ec1050b584eb6f36f105f256b8f4ac70d12e774d8b6dd91e2198", size = 2365685, upload-time = "2026-01-10T20:02:21.484Z" }, +] + +[[package]] +name = "gunicorn" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/13/ef67f59f6a7896fdc2c1d62b5665c5219d6b0a9a1784938eb9a28e55e128/gunicorn-25.1.0.tar.gz", hash = "sha256:1426611d959fa77e7de89f8c0f32eed6aa03ee735f98c01efba3e281b1c47616", size = 594377, upload-time = "2026-02-13T11:09:58.989Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/73/4ad5b1f6a2e21cf1e85afdaad2b7b1a933985e2f5d679147a1953aaa192c/gunicorn-25.1.0-py3-none-any.whl", hash = "sha256:d0b1236ccf27f72cfe14bce7caadf467186f19e865094ca84221424e839b8b8b", size = 197067, upload-time = "2026-02-13T11:09:57.146Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "psycopg" +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/b6/379d0a960f8f435ec78720462fd94c4863e7a31237cf81bf76d0af5883bf/psycopg-3.3.3.tar.gz", hash = "sha256:5e9a47458b3c1583326513b2556a2a9473a1001a56c9efe9e587245b43148dd9", size = 165624, upload-time = "2026-02-18T16:52:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/5b/181e2e3becb7672b502f0ed7f16ed7352aca7c109cfb94cf3878a9186db9/psycopg-3.3.3-py3-none-any.whl", hash = "sha256:f96525a72bcfade6584ab17e89de415ff360748c766f0106959144dcbb38c698", size = 212768, upload-time = "2026-02-18T16:46:27.365Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "pytest-django" +version = "4.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/2b/db9a193df89e5660137f5428063bcc2ced7ad790003b26974adf5c5ceb3b/pytest_django-4.12.0.tar.gz", hash = "sha256:df94ec819a83c8979c8f6de13d9cdfbe76e8c21d39473cfe2b40c9fc9be3c758", size = 91156, upload-time = "2026-02-14T18:40:49.235Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/a5/41d091f697c09609e7ef1d5d61925494e0454ebf51de7de05f0f0a728f1d/pytest_django-4.12.0-py3-none-any.whl", hash = "sha256:3ff300c49f8350ba2953b90297d23bf5f589db69545f56f1ec5f8cff5da83e85", size = 26123, upload-time = "2026-02-14T18:40:47.381Z" }, +] + +[[package]] +name = "pytest-sugar" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "termcolor" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/4e/60fed105549297ba1a700e1ea7b828044842ea27d72c898990510b79b0e2/pytest-sugar-1.1.1.tar.gz", hash = "sha256:73b8b65163ebf10f9f671efab9eed3d56f20d2ca68bda83fa64740a92c08f65d", size = 16533, upload-time = "2025-08-23T12:19:35.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/d5/81d38a91c1fdafb6711f053f5a9b92ff788013b19821257c2c38c1e132df/pytest_sugar-1.1.1-py3-none-any.whl", hash = "sha256:2f8319b907548d5b9d03a171515c1d43d2e38e32bd8182a1781eb20b43344cc8", size = 11440, upload-time = "2025-08-23T12:19:34.894Z" }, +] + +[[package]] +name = "pytv" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "django" }, + { name = "django-cors-headers" }, + { name = "django-environ" }, + { name = "django-ninja" }, + { name = "gunicorn" }, + { name = "psycopg" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-django" }, + { name = "pytest-sugar" }, +] + +[package.metadata] +requires-dist = [ + { name = "django", specifier = ">=6.0.3" }, + { name = "django-cors-headers", specifier = ">=4.9.0" }, + { name = "django-environ", specifier = ">=0.13.0" }, + { name = "django-ninja", specifier = ">=1.5.3" }, + { name = "gunicorn", specifier = ">=25.1.0" }, + { name = "psycopg", specifier = ">=3.3.3" }, + { name = "pytest", specifier = ">=9.0.2" }, + { name = "pytest-cov", specifier = ">=7.0.0" }, + { name = "pytest-django", specifier = ">=4.12.0" }, + { name = "pytest-sugar", specifier = ">=1.1.1" }, +] + +[[package]] +name = "sqlparse" +version = "0.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" }, +] + +[[package]] +name = "termcolor" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +]