feat(apple): add turnkey optional download provider

This commit is contained in:
joshpatra committed 2026-07-16 21:10:25 -04:00
1 parent 93bbd595fb
commit ec19acf928
40 files changed
+3196 -96

No files matched your search

+5
View File
@@ -21,6 +21,11 @@ ALLSTARR_IMAGE=ghcr.io/sopat712/allstarr:3.0.0-beta.1
APPLE_DOWNLOAD_URL=
APPLE_DOWNLOAD_QUALITY=alac-16-44
# Optional Apple profile build settings. `./allstarr.sh prepare-apple` writes the matching values.
APPLE_WRAPPER_TARGET_ARCH=x86_64
APPLE_WRAPPER_RUNTIME_PLATFORM=linux/amd64
APPLE_GATEWAY_STOREFRONT=us
# Optional AudioMuse-AI recommendation service. This is the AudioMuse base URL, not a Jellyfin URL.
# Allstarr checks /api/health and uses /api/sonic_fingerprint/generate. The current adapter does not
# support an AudioMuse authorization header, so leave this unset if the endpoint requires separate auth.
+8
View File
@@ -83,6 +83,13 @@ jobs:
- name: Test
run: dotnet test --configuration Release --no-build --verbosity normal
- name: Test Apple gateway contracts
working-directory: sidecars/apple-gateway
run: |
python -m pip install uv==0.11.29
UV_CACHE_DIR=/tmp/allstarr-uv-cache uv sync --frozen --extra test
UV_CACHE_DIR=/tmp/allstarr-uv-cache uv run --frozen --extra test pytest
- name: Validate Compose contracts
env:
SPOTIFY_API_SESSION_COOKIE: ci-placeholder
@@ -91,3 +98,4 @@ jobs:
docker compose -f docker-compose.yml -f docker-compose.dev.yml config --quiet
docker compose -f docker-compose.yml -f docker-compose.aio.yml config --quiet
docker compose -f docker-compose.yml -f docker-compose.spotify-lyrics.yml config --quiet
docker compose -f docker-compose.yml -f docker-compose.apple.yml config --quiet
+8
View File
@@ -84,11 +84,19 @@ jobs:
- name: Test
run: dotnet test --configuration Release --no-build --verbosity normal
- name: Test Apple gateway contracts
working-directory: sidecars/apple-gateway
run: |
python -m pip install uv==0.11.29
UV_CACHE_DIR=/tmp/allstarr-uv-cache uv sync --frozen --extra test
UV_CACHE_DIR=/tmp/allstarr-uv-cache uv run --frozen --extra test pytest
- name: Validate Compose contracts
run: |
docker compose -f docker-compose.yml config --quiet
docker compose -f docker-compose.yml -f docker-compose.dev.yml config --quiet
docker compose -f docker-compose.yml -f docker-compose.aio.yml config --quiet
docker compose -f docker-compose.yml -f docker-compose.apple.yml config --quiet
docker:
needs: build-and-test
+4
View File
@@ -70,6 +70,8 @@ obj/
/.env
/.env.before-*
/.allstarr-profiles
/.apple-provider/
/secrets/
/allstarr-backups/
/state-transfers/
@@ -95,6 +97,8 @@ __pycache__/
# Docker volumes
/valkey-data/
/.apple-provider/
/apple-downloads/
# Keep only the exact versioned OpenAPI artifact. Local feature notes, raw captures, cloned repositories,
# ad-hoc scripts, and credentials under apis/ are intentionally ignored.
+8 -5
View File
@@ -2,6 +2,9 @@
Start with [.env.example](.env.example). It is the checked-in list of deployment-facing settings for standard Compose. The dashboard can manage supported runtime settings, but secrets should still enter through protected bootstrap files or the encrypted provider-account flow, not source control.
For the turnkey install, optional-service, update, and custom-overlay commands, use the
[deployment profile guide](docs/operations/deployment-profiles.md).
`v3.0.0-beta.1` is a fresh-install baseline. Recreate the deployment instead of copying an old `.env` wholesale.
After the new database is ready, the administrator WebUI can preview and import the safe allowlisted subset without
replacing existing settings. Old Redis, mapping, extension, and job state is not imported automatically. Follow the
@@ -56,11 +59,11 @@ docker compose -f docker-compose.yml -f docker-compose.aio.yml up -d
Mounting that bundle does not bypass package state or permission review. Entries marked blocked remain inactive.
Apple downloads use a separately deployed compatible provider gateway. They are not part of Standard or AIO and
this repository does not build or update GAMDL or wrapper-v2 for the operator. Configure the gateway URL in the
dashboard or as `APPLE_DOWNLOAD_URL`. The URL must be the gateway's Allstarr-compatible HTTP API, not the raw
wrapper-v2 address. Adding or removing it does not replace the database, Valkey, application state, or media
volumes. Follow [the Apple download provider procedure](docs/operations/apple-download-provider.md).
Apple downloads are not part of Standard or AIO. The optional `docker-compose.apple.yml` profile builds Allstarr's
gateway with GAMDL 3.8.2 and the source-locked official wrapper-v2 0.0.2 checkout. It does not contain or download
Apple code. Prepare it with `./allstarr.sh prepare-apple FILE [ARCH]`, using a legally obtained compatible APK/APKM.
Adding or removing the profile does not replace the database, Valkey, application state, media volumes, or the
wrapper session volume. Follow [the Apple download provider procedure](docs/operations/apple-download-provider.md).
The optional Spotify lyrics service is likewise absent from Standard and AIO. Add the pinned
`docker-compose.spotify-lyrics.yml` overlay only when needed, following
+17 -17
View File
@@ -117,9 +117,9 @@ These defaults are confirmed product decisions:
- The SDK design must leave room for later automation, enrichment, recommendations, and UI extensions.
- Standard Docker Compose is the recommended default: core app, Postgres, and Valkey.
- AIO Compose remains available for the verified first-party package bundle; provider sidecars stay separate opt-ins.
- Allstarr does not bundle or manage GAMDL, wrapper-v2, or an Apple gateway in Standard or AIO. An operator may
connect a separately deployed compatible gateway by URL. This repository does not ship or maintain an Apple
gateway Compose overlay.
- Standard and AIO do not start Apple services. The separate Apple overlay builds the repository gateway with
GAMDL 3.8.2 and the source-locked official wrapper-v2 0.0.2 checkout after the operator supplies verified legal
Apple libraries. Removing the overlay degrades only Apple capabilities and preserves its session volume.
- Low-resource alternatives must be supported. Removing optional services should reduce capability, not break the app.
- Apple is split into `apple-download` and `apple-musickit`.
- Extension registry trust starts with checksum verification. Add signatures later.
@@ -172,7 +172,7 @@ Likely credentials for manual/live validation:
| Secrets | [allstarr/Core/Secrets](allstarr/Core/Secrets) | Provider-account records hold secret references. Versioned secret values are protected with AES-GCM using an external key ring, with replace, rotate, revoke, and tenant access rules. |
| Durable work | [allstarr/Core/Jobs](allstarr/Core/Jobs), [allstarr/Controllers/JobsController.cs](allstarr/Controllers/JobsController.cs) | Jobs, attempts, leases, idempotency keys, cancellation, retry state, sidecar deferrals, and transactional outbox messages live in the selected database. Users can inspect and cancel only their own jobs; admins can inspect all jobs. |
| Operations | [allstarr/Core/Operations](allstarr/Core/Operations), [allstarr/Controllers/DiagnosticsController.cs](allstarr/Controllers/DiagnosticsController.cs) | Liveness, readiness, sidecar capability state, redacted structured logs, correlated diagnostics, and Prometheus-style metrics expose the durable foundation without leaking credentials, media URLs, or account names. |
| Apple download gateway | [AppleMusicController.cs](allstarr/Controllers/AppleMusicController.cs), [Apple Music services](allstarr/Services/AppleMusic) | Allstarr calls one separately deployed compatible gateway by URL. The repository does not vendor GAMDL or wrapper-v2. The gateway contract, health probe, and runtime manifest decide which capabilities can be advertised. |
| Apple download gateway | [AppleMusicController.cs](allstarr/Controllers/AppleMusicController.cs), [Apple Music services](allstarr/Services/AppleMusic), [gateway](sidecars/apple-gateway) | The optional profile calls the repository gateway, which runs pinned GAMDL against a locked official wrapper-v2 build. The gateway contract, health probe, and runtime manifest decide which capabilities can be advertised. |
| Jellyfin protocol | [allstarr/Controllers/JellyfinController.Audio.cs](allstarr/Controllers/JellyfinController.Audio.cs), [allstarr/Controllers/JellyfinController.Search.cs](allstarr/Controllers/JellyfinController.Search.cs), [allstarr/Controllers/JellyfinController.PlaylistHandler.cs](allstarr/Controllers/JellyfinController.PlaylistHandler.cs) | Jellyfin compatibility stays the first protocol adapter. |
| Spotify playlists | [allstarr/Controllers/JellyfinController.Spotify.cs](allstarr/Controllers/JellyfinController.Spotify.cs), [docs/steering/SPOTIFY.md](docs/steering/SPOTIFY.md) | Durable provider-neutral playlist links are the current path. Spotify injection remains a compatibility path. |
| MusicBrainz | [allstarr/Services/MusicBrainz/MusicBrainzService.cs](allstarr/Services/MusicBrainz/MusicBrainzService.cs) | MusicBrainz contributes enrichment, canonical identity, matching, tagging, and local recommendation relationships. |
@@ -236,7 +236,8 @@ Prioritize structural improvements that make behavior easier to reason about: se
- Postgres: durable state for users, providers, health, libraries, matches, playlists, jobs, events, and job/outbox records in Docker deployments.
- SQLite: an explicit manual/small-install storage mode, not an automatic failover database.
- Valkey or Redis: cache, short-lived probe state, locks, and queue acceleration. It must not be the only durable record of a job or side effect.
- External services: optional lyrics tools, AudioMuse-AI, and provider-specific runtimes. Apple downloads use an operator-owned compatible gateway; GAMDL and wrapper-v2 are not repository services.
- External services: optional lyrics tools, AudioMuse-AI, and provider-specific runtimes. Apple downloads use the
separate source-locked Apple profile; GAMDL and wrapper-v2 are never part of Standard or AIO.
- Extensions: installed packages with manifests, capability declarations, permissions, settings, health checks, logs, and optional UI panels.
## Capability Contracts
@@ -319,7 +320,7 @@ Provider identifiers are lowercase, stable, and never inferred from display name
Provider split:
- `apple-download`: an operator-owned compatible gateway for download and optional streaming. A GAMDL-backed
- `apple-download`: the optional compatible gateway for download and download-backed streaming. Its GAMDL-backed
gateway can produce managed song, music-video, synced-lyrics, cover-art, and rich-tagging artifacts where its
advertised contract, account, source, and codec support them.
- `apple-musickit`: per-user MusicKit API access for personal-library playlists and playlist items, library songs/albums/artists, and documented library or favorite-state actions. It uses the Apple developer token plus that user's Music User Token.
@@ -813,8 +814,8 @@ Files:
- `docker-compose.yml`: core app, Postgres, Valkey.
- `docker-compose.aio.yml`: verified offline first-party package bundle, with no provider sidecars.
- `docker-compose.dev.yml`: local build override.
- The operator deploys any Apple/GAMDL provider gateway separately. It is not an Allstarr Compose file or part of
the Standard or AIO product contract. Allstarr receives only its URL and provider configuration.
- `docker-compose.apple.yml`: optional repository gateway plus the source-locked wrapper-v2 build. It remains
outside Standard and AIO and requires operator-supplied, hash-verified legal Apple libraries.
Lyrics and low-RAM overrides are ideas for later releases. They are not checked in, tested, or part of the current
version 3 beta deployment contract. Optional lyrics services run separately and connect by URL today.
@@ -832,8 +833,8 @@ docker compose -f docker-compose.yml -f docker-compose.aio.yml up -d
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build
```
An optional compatible Apple provider gateway is deployed and upgraded separately. After it is healthy, an admin adds its URL
as an `apple-download` provider in the WebUI. Allstarr probes the endpoint before activation and registers only
The optional Apple profile is prepared with `allstarr.sh prepare-apple` and started with the saved deployment
profile. Allstarr probes the endpoint before activation and registers only
the capabilities the compatible endpoint actually advertises and passes contract checks for. Provider cards,
source selectors, routing priority controls, jobs, and diagnostics show each capability as available, degraded,
unsupported, disabled, or needing configuration. Removing the endpoint disables only those routes. Re-adding a
@@ -1066,9 +1067,9 @@ Phase 5 replaces trusted extension folders with a reviewed SDK v1 provider runti
- The admin UI manages registries, checksum staging, per-permission approval or denial, activation, disable,
rollback, retained-account uninstall, logs, revisions, and failure state without exposing package paths,
manifests, content hashes, or credentials. The author guide is [Extension SDK v1](docs/extensions/sdk-v1.md).
- The Apple download provider uses a URL-configured external gateway. Allstarr validates its API, health,
authentication state, and capability manifest but does not build, publish, update, or roll back the gateway.
Standard and AIO do not include GAMDL or wrapper-v2.
- The Apple download provider uses the optional repository gateway and typed managed-artifact adapter. Allstarr
validates its API, health, authentication state, capability manifest, MIME, size, checksum, and workspace
ownership. Standard and AIO still do not include GAMDL or wrapper-v2.
The Phase 5 exit gate passed 1,249 .NET tests with no skips. The focused extension, registry, router, host,
storage, and WebUI gate passed 133 tests; the final capability-adapter gate passed 31 tests. The release build has
@@ -1206,7 +1207,7 @@ The final pass reconciles the repository with the implemented version 3 beta bas
provenance or an explicit `not-declared` status.
Production-derived captures and private clone material are explicitly prohibited.
- Standard, development, and AIO Compose files render. Standard stays small and AIO adds only the checksum-locked
offline first-party bundle. Apple downloads are an independent external gateway configured by URL.
offline first-party bundle. Apple downloads are an independent optional, source-locked Compose profile.
- The user-visible provider support matrix now reflects durable Last.fm and ListenBrainz scrobbling, Last.fm and
ListenBrainz recommendations, MusicBrainz-informed local similarity, managed enrichment, and the real SDK v1
capability boundary.
@@ -1280,12 +1281,11 @@ Exit: a user can review a match and playlist preview, run duplicate-safe materia
- Expand `ExtensionManager` into the documented constrained provider runtime.
- Add registry management without default third-party registries, mandatory registry checksum verification, permission review, logs, staged updates, rollback, and route integration.
- Add version-pinned external-service compatibility checks, contract tests, capability diffs, and rollback guidance. Do not use `docker compose up` as a source-code updater.
- Make the operator-owned compatible gateway an optional external `apple-download` provider configured by URL in the WebUI. Discover and
- Make the compatible gateway an optional `apple-download` provider. Discover and
verify its API version, authentication state, health, and per-feature capability manifest before registration.
Display those capabilities and their recovery actions consistently in provider cards, source and priority
controls, jobs, diagnostics, and routing decisions. Standard and AIO must work without it. Document how to add,
replace, disable, and re-add the endpoint later without database or media loss. Do not ship or maintain its
Compose stack in this repository.
replace, disable, and re-add the profile later without database, session, or media loss.
- Add fake and malicious-package coverage plus extension-authoring documentation.
Exit: an enabled test provider participates in routing only after its package, permissions, health, and account scope pass validation.
+14 -17
View File
@@ -19,26 +19,20 @@ Standard Compose runs Allstarr, Postgres, and Valkey. It exposes one client prot
```bash
git clone https://github.com/SoPat712/allstarr.git
cd allstarr
cp .env.example .env
mkdir -p secrets downloads kept
umask 077
openssl rand -base64 32 > secrets/postgres-password.txt
key="$(openssl rand -base64 32)"
printf '{"activeKeyId":"key-1","keys":{"key-1":"%s"}}\n' "$key" > secrets/allstarr-keyring.json
unset key
chmod 600 .env secrets/postgres-password.txt secrets/allstarr-keyring.json
./allstarr.sh init
```
Edit `.env`. At minimum, select `BACKEND_TYPE`, set the matching backend URL, and review the image tag and mounted paths. Jellyfin server-side library operations also need its API key and user ID.
```bash
docker compose config --quiet
docker compose pull
docker compose up -d
docker compose ps
./allstarr.sh up
curl --fail http://127.0.0.1:5274/health/ready
```
`allstarr.sh` remembers optional profiles, validates the merged Compose model, creates secrets with private file
permissions, and never deletes volumes. Normal upgrades are `./allstarr.sh update`; they pull reviewed images and
recreate the saved profile without requiring users to remember a growing list of `-f` arguments.
The standard stack is the smaller, recommended default. The AIO override mounts the checksum-locked offline
first-party package bundle, but it does not force optional provider sidecars on anyone:
@@ -49,13 +43,15 @@ docker compose -f docker-compose.yml -f docker-compose.aio.yml up -d
The bundle lock is still authoritative. A bundled package marked blocked is not staged or activated merely because
the AIO files are mounted.
Apple downloads are optional and are not bundled with Standard or AIO. Run a compatible Apple provider gateway
separately, then give Allstarr its URL through the dashboard or `APPLE_DOWNLOAD_URL`. The URL must point to the
gateway API, not directly to wrapper-v2. Removing that URL disables Apple download routes without changing Postgres
or media volumes. See [Apple download provider setup](docs/operations/apple-download-provider.md).
Apple downloads are optional and are not bundled with Standard or AIO. The Apple profile builds the repository's
small gateway with GAMDL 3.8.2 and the official wrapper-v2 0.0.2 source. Allstarr never supplies Apple binaries, so
the operator provides one legally obtained compatible APK/APKM and runs
`./allstarr.sh prepare-apple /path/to/file.apkm`. Removing the profile disables Apple download routes without
changing Postgres, media, or the persistent wrapper login session. See
[Apple download provider setup](docs/operations/apple-download-provider.md).
Spotify lyrics are optional too. To run the pinned private-network sidecar, add
`docker-compose.spotify-lyrics.yml` to the Compose command and follow the
it with `./allstarr.sh enable spotify`, then run `./allstarr.sh up`. Follow the
[Spotify lyrics sidecar guide](docs/operations/spotify-lyrics-sidecar.md). Importing an old `.env` can restore the
endpoint URL, but it cannot start the sidecar or pass a cookie to it.
@@ -100,6 +96,7 @@ Allstarr supports Jellyfin clients and Subsonic/OpenSubsonic clients through the
- [Configuration](CONFIGURATION.md)
- [Client compatibility](CLIENTS.md)
- [Storage operations](docs/operations/storage.md)
- [Deployment profiles and optional services](docs/operations/deployment-profiles.md)
- [Extension SDK](docs/extensions/sdk-v1.md)
- [Contributing](CONTRIBUTING.md)
- [Implementation charter and phase history](OVERHAUL.md)
@@ -0,0 +1,203 @@
using System.Net;
using System.Security.Cryptography;
using System.Text;
using allstarr.Core.Capabilities;
using allstarr.Core.Downloads;
using allstarr.Core.Providers.AppleDownload;
using allstarr.Models.Settings;
using allstarr.Services.AppleMusic;
using Microsoft.Extensions.Options;
namespace allstarr.Tests;
public sealed class AppleDownloadCapabilityAdapterTests : IDisposable
{
private readonly string root = Path.Combine(Path.GetTempPath(), "allstarr-apple-capability", Guid.NewGuid().ToString("N"));
[Fact]
public async Task Download_UsesDiscoveredGatewayAndResolvesHostOwnedArtifact()
{
var audio = Encoding.UTF8.GetBytes("fake flac audio bytes");
var gateway = new GatewayHandler(audio, "audio/flac");
var settings = new AppleDownloadSettings { BaseUrl = "https://gateway.test/", Quality = "alac-16-44" };
var client = new HttpClient(gateway);
var discovery = new AppleDownloadEndpointDiscovery(
new StaticClientFactory(client), Options.Create(settings));
var store = new MemoryStore();
var resolver = new ProviderDownloadArtifactResolver(store, new() { RootPath = root });
var adapter = new AppleDownloadCapabilityAdapter(client, settings, discovery, resolver, 1024 * 1024);
var tenant = Guid.CreateVersion7();
var user = Guid.CreateVersion7();
var job = Guid.CreateVersion7();
var workspace = await resolver.CreateWorkspaceAsync(new(
tenant, user, job, AppleDownloadCapabilityAdapter.StableProviderId, null, "favorite:apple-track"));
var context = Context(tenant, user);
var track = new ProviderExternalResourceId(
AppleDownloadCapabilityAdapter.StableProviderId, ProviderResourceKind.Track, "apple/track 1");
var availability = await adapter.CheckAvailabilityAsync(context, new(track));
var outcome = await adapter.DownloadAsync(context, new(
track, job, workspace.Reference, ProviderAudioQuality.Lossless));
Assert.True(availability.IsSuccess);
Assert.Equal(ProviderDownloadAvailabilityState.Available, availability.RequireValue().State);
var output = outcome.RequireValue();
Assert.EndsWith(".flac", output.ArtifactId, StringComparison.Ordinal);
Assert.DoesNotContain("apple/track", output.ArtifactId, StringComparison.Ordinal);
Assert.Equal(audio.Length, output.SizeBytes);
Assert.Equal(Convert.ToHexString(SHA256.HashData(audio)).ToLowerInvariant(), output.Sha256);
Assert.Equal("flac", output.Media.Codec);
var verified = await resolver.ResolveAsync(workspace.Reference, output);
Assert.Equal(audio, await File.ReadAllBytesAsync(verified.SourcePath));
Assert.Equal(ProviderDownloadArtifactState.Verified, verified.State);
Assert.Single(store.Artifacts);
Assert.Contains(gateway.Requests, uri =>
uri.PathAndQuery == "/api/download/apple%2Ftrack%201?quality=alac-16-44");
}
[Fact]
public async Task Download_RejectsUnrecognizedMediaBeforeWritingArtifact()
{
var gateway = new GatewayHandler(Encoding.UTF8.GetBytes("not audio"), "text/html");
var settings = new AppleDownloadSettings { BaseUrl = "https://gateway.test/" };
var client = new HttpClient(gateway);
var discovery = new AppleDownloadEndpointDiscovery(
new StaticClientFactory(client), Options.Create(settings));
var store = new MemoryStore();
var resolver = new ProviderDownloadArtifactResolver(store, new() { RootPath = root });
var adapter = new AppleDownloadCapabilityAdapter(client, settings, discovery, resolver, 1024);
var tenant = Guid.CreateVersion7();
var user = Guid.CreateVersion7();
var job = Guid.CreateVersion7();
var workspace = await resolver.CreateWorkspaceAsync(new(
tenant, user, job, AppleDownloadCapabilityAdapter.StableProviderId, null, "favorite:bad-media"));
var track = new ProviderExternalResourceId(
AppleDownloadCapabilityAdapter.StableProviderId, ProviderResourceKind.Track, "bad-media");
var outcome = await adapter.DownloadAsync(Context(tenant, user), new(
track, job, workspace.Reference, ProviderAudioQuality.Any));
Assert.False(outcome.IsSuccess);
Assert.Equal(ProviderErrorKind.IncompatibleMedia, outcome.Error!.Kind);
Assert.Empty(Directory.EnumerateFiles(Path.Combine(root, workspace.Reference.WorkspaceId)));
Assert.Empty(store.Artifacts);
}
[Fact]
public async Task Availability_DoesNotAdvertiseDownloadWhenManifestOmitsRoute()
{
var gateway = new GatewayHandler([], "audio/flac") { AdvertiseDownload = false };
var settings = new AppleDownloadSettings { BaseUrl = "https://gateway.test/" };
var client = new HttpClient(gateway);
var discovery = new AppleDownloadEndpointDiscovery(
new StaticClientFactory(client), Options.Create(settings));
var resolver = new ProviderDownloadArtifactResolver(new MemoryStore(), new() { RootPath = root });
var adapter = new AppleDownloadCapabilityAdapter(client, settings, discovery, resolver, 1024);
var tenant = Guid.CreateVersion7();
var user = Guid.CreateVersion7();
var track = new ProviderExternalResourceId(
AppleDownloadCapabilityAdapter.StableProviderId, ProviderResourceKind.Track, "missing-route");
var outcome = await adapter.CheckAvailabilityAsync(Context(tenant, user), new(track));
Assert.Equal(ProviderDownloadAvailabilityState.Unavailable, outcome.RequireValue().State);
}
private static ProviderExecutionContext Context(Guid tenant, Guid user) => new(
new ProviderActorContext(tenant, ProviderActorKind.User, user,
new ProviderBackendPrincipal("jellyfin", "primary", "user")),
AppleDownloadCapabilityAdapter.StableProviderId,
account: null,
library: null,
new ProviderExecutionPolicy(
new ProviderQualityPolicy(ProviderAudioQuality.Any, ProviderAudioQuality.HighResolution, true),
ProviderExplicitContentPolicy.Allow,
allowFallback: true,
allowSharedAccount: false,
allowManagedDownloads: true,
[AppleDownloadCapabilityAdapter.StableProviderId]),
"download-test",
"correlation-test",
DateTimeOffset.UtcNow.AddMinutes(1),
CancellationToken.None,
"favorite:test");
public void Dispose()
{
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
}
private sealed class StaticClientFactory(HttpClient client) : IHttpClientFactory
{
public HttpClient CreateClient(string name) => client;
}
private sealed class GatewayHandler(byte[] audio, string contentType) : HttpMessageHandler
{
public bool AdvertiseDownload { get; init; } = true;
public List<Uri> Requests { get; } = [];
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
Requests.Add(request.RequestUri!);
var path = request.RequestUri!.AbsolutePath;
HttpResponseMessage response = path switch
{
"/api/capabilities" => Json($$"""
{"sidecarApiVersion":"1.0","capabilities":[
{"id":"metadata-search-song","state":"supported"},
{"id":"metadata-song","state":"supported"},
{"id":"stream-audio-song","state":"supported"}
{{(AdvertiseDownload ? ",{\"id\":\"download-audio-song\",\"state\":\"supported\"}" : string.Empty)}}
]}
"""),
"/api/health" => Json("{\"staged\":true,\"daemon_running\":true,\"wrapper_healthy\":true,\"logged_in\":true}"),
"/api/me" => Json("{\"authenticated\":true}"),
_ when path.StartsWith("/api/download/", StringComparison.Ordinal) => Audio(),
_ => new(HttpStatusCode.NotFound)
};
response.RequestMessage = request;
return Task.FromResult(response);
}
private HttpResponseMessage Audio()
{
var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(audio) };
response.Content.Headers.ContentType = new(contentType);
return response;
}
private static HttpResponseMessage Json(string body) => new(HttpStatusCode.OK)
{
Content = new StringContent(body, Encoding.UTF8, "application/json")
};
}
private sealed class MemoryStore : IProviderDownloadArtifactStore
{
public List<ProviderDownloadWorkspaceEntity> Workspaces { get; } = [];
public List<ProviderDownloadArtifactEntity> Artifacts { get; } = [];
public Task<ProviderDownloadWorkspaceEntity> CreateWorkspaceAsync(ProviderDownloadWorkspaceEntity value, CancellationToken token)
{
var existing = Workspaces.SingleOrDefault(item => item.WorkspaceId == value.WorkspaceId);
if (existing != null) return Task.FromResult(existing);
Workspaces.Add(value);
return Task.FromResult(value);
}
public Task<ProviderDownloadWorkspaceEntity?> GetWorkspaceAsync(string id, CancellationToken token) =>
Task.FromResult(Workspaces.SingleOrDefault(item => item.WorkspaceId == id));
public Task<ProviderDownloadArtifactEntity> AddVerifiedAsync(ProviderDownloadArtifactEntity value, CancellationToken token)
{
var existing = Artifacts.SingleOrDefault(item =>
item.WorkspaceRecordId == value.WorkspaceRecordId && item.ProviderArtifactId == value.ProviderArtifactId);
if (existing != null) return Task.FromResult(existing);
Artifacts.Add(value);
return Task.FromResult(value);
}
public Task<ProviderDownloadArtifactEntity?> FindByJobAsync(Guid tenantId, Guid jobId, string provider, CancellationToken token) =>
Task.FromResult(Artifacts.SingleOrDefault(item =>
item.TenantId == tenantId && item.DurableJobId == jobId && item.ProviderId == provider));
public Task MarkPlacedAsync(Guid id, Guid managedId, CancellationToken token) => Task.CompletedTask;
}
}
@@ -2,6 +2,10 @@ using allstarr.Core.Capabilities;
using allstarr.Core.Providers;
using allstarr.Core.Providers.Deezer;
using allstarr.Core.Providers.AppleMusicKit;
using allstarr.Core.Providers.AppleDownload;
using allstarr.Core.Downloads;
using allstarr.Models.Settings;
using allstarr.Services.AppleMusic;
using allstarr.Core.Providers.Spotify;
using allstarr.Services;
using Moq;
@@ -21,11 +25,20 @@ public sealed class BuiltInProviderDescriptorCatalogTests
var spotify = new SpotifyPlaylistCapabilityAdapter(
new HttpClient(new Mock<HttpMessageHandler>().Object),
new Mock<IProviderAccountSecretAccessor>(MockBehavior.Strict).Object);
var appleDownload = new AppleDownloadCapabilityAdapter(
new HttpClient(new Mock<HttpMessageHandler>().Object),
new AppleDownloadSettings(),
new Mock<IAppleDownloadEndpointDiscovery>(MockBehavior.Strict).Object,
new ProviderDownloadArtifactResolver(
new Mock<IProviderDownloadArtifactStore>(MockBehavior.Strict).Object,
new ProviderDownloadWorkspaceOptions { RootPath = Path.GetTempPath() }),
1024);
var registry = new ProviderRegistry(
[
DeezerMetadataCapabilityAdapter.CreateRegistration(deezer),
AppleMusicKitPlaylistCapabilityAdapter.CreateRegistration(apple),
SpotifyPlaylistCapabilityAdapter.CreateRegistration(spotify),
AppleDownloadCapabilityAdapter.CreateRegistration(appleDownload),
.. BuiltInProviderDescriptorCatalog.LegacyRegistrations
]);
+32
View File
@@ -102,6 +102,29 @@ public sealed class ComposeContractTests
Assert.DoesNotContain("/var/run/docker.sock", overlay, StringComparison.Ordinal);
}
[Fact]
public void AppleOverlay_IsOptionalSourceLockedAndPrivate()
{
RenderCompose("docker-compose.yml", "docker-compose.apple.yml");
var overlay = File.ReadAllText(Path.Combine(_repositoryRoot, "docker-compose.apple.yml"));
var gatewayDockerfile = File.ReadAllText(Path.Combine(
_repositoryRoot, "sidecars", "apple-gateway", "Dockerfile"));
var gatewayProject = File.ReadAllText(Path.Combine(
_repositoryRoot, "sidecars", "apple-gateway", "pyproject.toml"));
var sourceLock = File.ReadAllText(Path.Combine(
_repositoryRoot, "tools", "apple-provider", "source-lock.json"));
Assert.Contains("AppleDownload__BaseUrl: http://apple-gateway:8000", overlay, StringComparison.Ordinal);
Assert.Contains("APPLE_GATEWAY_WRAPPER_DECRYPT_PORT: 10020", overlay, StringComparison.Ordinal);
Assert.Contains("./.apple-provider/wrapper-v2", overlay, StringComparison.Ordinal);
Assert.Contains("apple-wrapper-session:", overlay, StringComparison.Ordinal);
Assert.DoesNotContain("ports:", overlay, StringComparison.Ordinal);
Assert.DoesNotContain("/var/run/docker.sock", overlay, StringComparison.Ordinal);
Assert.Contains("python:3.12.11-slim-bookworm@sha256:", gatewayDockerfile, StringComparison.Ordinal);
Assert.Contains("gamdl==3.8.2", gatewayProject, StringComparison.Ordinal);
Assert.Contains("0bc16acb55f557b5c98d49f21d7af685410f7f8b", sourceLock, StringComparison.Ordinal);
}
[Fact]
public void RuntimeImage_ContainsBackupToolsAndPinnedDotnetBases()
{
@@ -144,6 +167,11 @@ public sealed class ComposeContractTests
"docker compose -f docker-compose.yml -f docker-compose.spotify-lyrics.yml config --quiet",
workflow,
StringComparison.Ordinal);
Assert.Contains(
"docker compose -f docker-compose.yml -f docker-compose.apple.yml config --quiet",
workflow,
StringComparison.Ordinal);
Assert.Contains("uv sync --frozen --extra test", workflow, StringComparison.Ordinal);
Assert.DoesNotContain("continue-on-error: true", workflow, StringComparison.Ordinal);
}
@@ -183,6 +211,10 @@ public sealed class ComposeContractTests
"docker compose -f docker-compose.yml -f docker-compose.aio.yml config --quiet",
workflow,
StringComparison.Ordinal);
Assert.Contains(
"docker compose -f docker-compose.yml -f docker-compose.apple.yml config --quiet",
workflow,
StringComparison.Ordinal);
Assert.Contains("needs: build-and-test", workflow, StringComparison.Ordinal);
Assert.Contains(
"uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7",
Executable
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "${BASH_SOURCE[0]%/*}" && pwd)"
PROFILE_FILE="$ROOT/.allstarr-profiles"
die() { echo "allstarr: $*" >&2; exit 1; }
need() { command -v "$1" >/dev/null 2>&1 || die "$1 is required"; }
profiles() {
local values=(standard)
if [[ -f "$PROFILE_FILE" ]]; then
while IFS= read -r value; do
case "$value" in spotify|apple|aio) values+=("$value") ;; esac
done < "$PROFILE_FILE"
fi
printf '%s\n' "${values[@]}"
}
compose_args() {
COMPOSE=(-f "$ROOT/docker-compose.yml")
while IFS= read -r profile; do
case "$profile" in
spotify) COMPOSE+=(-f "$ROOT/docker-compose.spotify-lyrics.yml") ;;
apple) COMPOSE+=(-f "$ROOT/docker-compose.apple.yml") ;;
aio) COMPOSE+=(-f "$ROOT/docker-compose.aio.yml") ;;
esac
done < <(profiles)
}
remember_profile() {
local wanted="$1"
touch "$PROFILE_FILE"
grep -qxF "$wanted" "$PROFILE_FILE" 2>/dev/null || printf '%s\n' "$wanted" >> "$PROFILE_FILE"
}
forget_profile() {
local unwanted="$1" temporary
temporary="$(mktemp)"
if [[ -f "$PROFILE_FILE" ]]; then
grep -vxF "$unwanted" "$PROFILE_FILE" > "$temporary" || true
fi
mv "$temporary" "$PROFILE_FILE"
}
init() {
need docker
need openssl
docker compose version >/dev/null
[[ -f "$ROOT/.env" ]] || cp "$ROOT/.env.example" "$ROOT/.env"
install -d -m 700 "$ROOT/secrets"
install -d -m 755 "$ROOT/downloads" "$ROOT/kept"
if [[ ! -s "$ROOT/secrets/postgres-password.txt" ]]; then
umask 077
openssl rand -base64 36 > "$ROOT/secrets/postgres-password.txt"
fi
if [[ ! -s "$ROOT/secrets/allstarr-keyring.json" ]]; then
umask 077
local key
key="$(openssl rand -base64 32)"
printf '{"activeKeyId":"key-1","keys":{"key-1":"%s"}}\n' "$key" > "$ROOT/secrets/allstarr-keyring.json"
fi
chmod 600 "$ROOT/secrets/postgres-password.txt" "$ROOT/secrets/allstarr-keyring.json"
touch "$PROFILE_FILE"
echo "Allstarr is initialized. Edit .env, then run: ./allstarr.sh up"
}
prepare_apple() {
local bundle="${1:-}" arch="${2:-x86_64}" runtime="linux/amd64"
[[ -f "$ROOT/.env" ]] || die "run ./allstarr.sh init before enabling providers"
[[ -n "$bundle" && -f "$bundle" ]] || die "usage: ./allstarr.sh prepare-apple /path/to/apple-music.apk[m] [x86_64|arm64-v8a]"
case "$arch" in
x86_64) ;;
arm64-v8a) runtime=linux/arm64 ;;
*) die "Apple architecture must be x86_64 or arm64-v8a" ;;
esac
bash "$ROOT/tools/apple-provider/prepare.sh" --apkm "$bundle" --arch "$arch"
if grep -q '^APPLE_WRAPPER_TARGET_ARCH=' "$ROOT/.env"; then
sed -i.bak "s|^APPLE_WRAPPER_TARGET_ARCH=.*|APPLE_WRAPPER_TARGET_ARCH=$arch|" "$ROOT/.env"
sed -i.bak "s|^APPLE_WRAPPER_RUNTIME_PLATFORM=.*|APPLE_WRAPPER_RUNTIME_PLATFORM=$runtime|" "$ROOT/.env"
rm -f "$ROOT/.env.bak"
else
printf '\nAPPLE_WRAPPER_TARGET_ARCH=%s\nAPPLE_WRAPPER_RUNTIME_PLATFORM=%s\n' "$arch" "$runtime" >> "$ROOT/.env"
fi
remember_profile apple
echo "Apple provider source and verified native libraries are ready. Run: ./allstarr.sh up"
}
up() {
compose_args
docker compose "${COMPOSE[@]}" config --quiet
if profiles | grep -qx apple; then
docker compose "${COMPOSE[@]}" up -d --build --remove-orphans
else
docker compose "${COMPOSE[@]}" up -d --remove-orphans
fi
docker compose "${COMPOSE[@]}" ps
}
update() {
compose_args
docker compose "${COMPOSE[@]}" config --quiet
docker compose "${COMPOSE[@]}" pull --ignore-buildable
if profiles | grep -qx apple; then
docker compose "${COMPOSE[@]}" build apple-wrapper apple-gateway
fi
docker compose "${COMPOSE[@]}" up -d --remove-orphans
docker compose "${COMPOSE[@]}" ps
}
usage() {
cat <<'EOF'
Usage: ./allstarr.sh COMMAND
init Create .env, directories, and secrets
up Start the saved deployment profile
update Pull reviewed images and safely recreate
status Show containers and the saved profile
logs [service] Follow redacted container logs
enable spotify|aio Add an optional saved profile
disable spotify|apple|aio Remove an optional profile on next up
prepare-apple FILE [ARCH] Verify/stage a legal APK/APKM and enable Apple
down Stop containers without deleting data
Optional profiles are saved in .allstarr-profiles. No command deletes volumes,
Postgres data, managed music, provider sessions, or imported settings.
EOF
}
command="${1:-help}"
shift || true
cd "$ROOT"
case "$command" in
init) init ;;
up) up ;;
update) update ;;
status) compose_args; echo "Profiles: $(profiles | paste -sd, -)"; docker compose "${COMPOSE[@]}" ps ;;
logs) compose_args; docker compose "${COMPOSE[@]}" logs --tail=200 -f "$@" ;;
enable)
case "${1:-}" in
spotify|aio) remember_profile "$1" ;;
apple) die "use prepare-apple for Apple so its libraries are verified first" ;;
*) die "choose spotify or aio" ;;
esac
echo "Profile enabled. Run: ./allstarr.sh up"
;;
disable)
case "${1:-}" in spotify|apple|aio) forget_profile "$1" ;; *) die "choose spotify, apple, or aio" ;; esac
echo "Profile disabled. Run ./allstarr.sh up to apply; stored data is preserved."
;;
down) compose_args; docker compose "${COMPOSE[@]}" down ;;
help|-h|--help) usage ;;
*) usage; exit 2 ;;
esac
@@ -32,6 +32,76 @@ public sealed class ProviderDownloadArtifactResolver(IProviderDownloadArtifactSt
return new(entity.Id, new ProviderManagedWorkspaceReference(entity.WorkspaceId));
}
/// <summary>
/// Copies provider bytes into a registered private workspace. Providers never receive
/// a host filesystem path, and the returned contract is derived from bytes the host wrote.
/// </summary>
public async Task<ProviderDownloadArtifactWriteResult> WriteAsync(
ProviderDownloadArtifactWriteRequest request,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
if (request.DurableJobId == Guid.Empty || string.IsNullOrWhiteSpace(request.ProviderId) ||
request.MaximumBytes < 1 || request.ExpectedBytes is < 0)
throw new ArgumentException("Download artifact write constraints are invalid.", nameof(request));
if (request.ExpectedBytes > request.MaximumBytes)
throw new InvalidDataException("The provider download exceeds the managed artifact size limit.");
var persistedWorkspace = await store.GetWorkspaceAsync(request.Workspace.WorkspaceId, cancellationToken)
?? throw new InvalidOperationException("The provider download workspace is not registered.");
var providerId = request.ProviderId.Trim().ToLowerInvariant();
if (persistedWorkspace.DurableJobId != request.DurableJobId ||
!persistedWorkspace.ProviderId.Equals(providerId, StringComparison.Ordinal))
throw new UnauthorizedAccessException("The provider download does not belong to this workspace.");
var relative = NormalizeArtifactReference(request.ArtifactId);
var workspaceRoot = Contained(WorkspaceRoot(), persistedWorkspace.WorkspaceId);
RejectSymlink(workspaceRoot);
var destination = Contained(workspaceRoot, relative);
var parent = Path.GetDirectoryName(destination)!;
Directory.CreateDirectory(parent);
RejectPathSymlinks(workspaceRoot, parent);
if (File.Exists(destination))
return await DescribeExistingAsync(relative, destination, request.MaximumBytes, cancellationToken);
var partial = destination + ".partial-" + Guid.NewGuid().ToString("N");
try
{
await using (var output = new FileStream(partial, FileMode.CreateNew, FileAccess.Write, FileShare.None,
128 * 1024, FileOptions.Asynchronous | FileOptions.SequentialScan))
using (var hasher = IncrementalHash.CreateHash(HashAlgorithmName.SHA256))
{
var buffer = new byte[128 * 1024];
long written = 0;
while (true)
{
var read = await request.Content.ReadAsync(buffer, cancellationToken);
if (read == 0) break;
written = checked(written + read);
if (written > request.MaximumBytes)
throw new InvalidDataException("The provider download exceeds the managed artifact size limit.");
await output.WriteAsync(buffer.AsMemory(0, read), cancellationToken);
hasher.AppendData(buffer, 0, read);
request.Progress?.Invoke(written, request.ExpectedBytes);
}
if (written < 1)
throw new InvalidDataException("The provider returned an empty download artifact.");
if (request.ExpectedBytes.HasValue && written != request.ExpectedBytes.Value)
throw new InvalidDataException("The provider download length does not match its response contract.");
await output.FlushAsync(cancellationToken);
output.Close();
File.Move(partial, destination, overwrite: false);
return new(relative, Convert.ToHexString(hasher.GetHashAndReset()).ToLowerInvariant(), written);
}
}
catch
{
if (File.Exists(partial)) File.Delete(partial);
throw;
}
}
public async Task<VerifiedProviderDownloadArtifact> ResolveAsync(ProviderManagedWorkspaceReference workspace,
ProviderDownloadedArtifact output, CancellationToken cancellationToken = default)
{
@@ -154,6 +224,22 @@ public sealed class ProviderDownloadArtifactResolver(IProviderDownloadArtifactSt
}
}
private static async Task<ProviderDownloadArtifactWriteResult> DescribeExistingAsync(
string relative,
string path,
long maximumBytes,
CancellationToken cancellationToken)
{
RejectSymlink(path);
var info = new FileInfo(path);
if (info.Length < 1 || info.Length > maximumBytes)
throw new InvalidDataException("The existing managed artifact has an invalid size.");
await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 128 * 1024,
FileOptions.Asynchronous | FileOptions.SequentialScan);
var hash = await SHA256.HashDataAsync(stream, cancellationToken);
return new(relative, Convert.ToHexString(hash).ToLowerInvariant(), info.Length);
}
private static VerifiedProviderDownloadArtifact Result(ProviderDownloadArtifactEntity item, string path) => new(
item.Id, item.WorkspaceRecordId, path, item.ContentSha256, item.Length, item.TenantId, item.OwnerUserId,
item.DurableJobId, item.ProviderId, item.ProviderAccountId, item.State, item.ManagedFileId)
@@ -52,6 +52,23 @@ public sealed record ProviderDownloadWorkspaceRequest(Guid TenantId, Guid? Owner
public sealed record ProviderDownloadWorkspace(Guid RecordId, ProviderManagedWorkspaceReference Reference);
public sealed record ProviderDownloadArtifactWriteRequest(
ProviderManagedWorkspaceReference Workspace,
Guid DurableJobId,
string ProviderId,
string ArtifactId,
Stream Content,
long MaximumBytes)
{
public long? ExpectedBytes { get; init; }
public Action<long, long?>? Progress { get; init; }
}
public sealed record ProviderDownloadArtifactWriteResult(
string ArtifactId,
string Sha256,
long SizeBytes);
public sealed record VerifiedProviderDownloadArtifact(Guid Id, Guid WorkspaceRecordId, [property: JsonIgnore] string SourcePath,
string ContentSha256, long Length, Guid TenantId, Guid? OwnerUserId, Guid DurableJobId,
string ProviderId, Guid? ProviderAccountId, ProviderDownloadArtifactState State, Guid? ManagedFileId)
@@ -63,6 +80,7 @@ public sealed class ProviderDownloadWorkspaceOptions
{
public const string SectionName = "Downloads:Workspace";
public string RootPath { get; set; } = "./downloads/workspaces";
public long MaximumArtifactBytes { get; set; } = 2L * 1024 * 1024 * 1024;
}
public interface IProviderDownloadArtifactStore
@@ -0,0 +1,281 @@
using System.Net;
using System.Security.Cryptography;
using System.Text;
using allstarr.Core.Capabilities;
using allstarr.Core.Downloads;
using allstarr.Models.Settings;
using allstarr.Services.AppleMusic;
using allstarr.Services.Common;
using Microsoft.Extensions.Options;
namespace allstarr.Core.Providers.AppleDownload;
public sealed class AppleDownloadCapabilityAdapter : IProviderDownloadCapability
{
public const string StableProviderId = "apple-download";
public const string HttpClientName = "AppleDownloadCapability";
private readonly HttpClient http;
private readonly AppleDownloadSettings settings;
private readonly IAppleDownloadEndpointDiscovery discovery;
private readonly ProviderDownloadArtifactResolver artifacts;
private readonly long maximumArtifactBytes;
[Microsoft.Extensions.DependencyInjection.ActivatorUtilitiesConstructor]
public AppleDownloadCapabilityAdapter(
IHttpClientFactory clients,
IOptions<AppleDownloadSettings> settings,
IAppleDownloadEndpointDiscovery discovery,
ProviderDownloadArtifactResolver artifacts,
ProviderDownloadWorkspaceOptions workspaceOptions)
: this(clients.CreateClient(HttpClientName), settings.Value, discovery, artifacts,
workspaceOptions.MaximumArtifactBytes)
{
}
public AppleDownloadCapabilityAdapter(
HttpClient http,
AppleDownloadSettings settings,
IAppleDownloadEndpointDiscovery discovery,
ProviderDownloadArtifactResolver artifacts,
long maximumArtifactBytes)
{
this.http = http;
this.settings = settings;
this.discovery = discovery;
this.artifacts = artifacts;
this.maximumArtifactBytes = maximumArtifactBytes > 0
? maximumArtifactBytes
: throw new ArgumentOutOfRangeException(nameof(maximumArtifactBytes));
}
public string ProviderId => StableProviderId;
public ProviderCapabilityKind Capability => ProviderCapabilityKind.Download;
public async Task<ProviderOutcome<ProviderDownloadAvailability>> CheckAvailabilityAsync(
ProviderExecutionContext context,
ProviderDownloadAvailabilityRequest request)
{
var error = Validate(context, request.TrackId);
if (error != null) return ProviderOutcome<ProviderDownloadAvailability>.Failure(error);
try
{
var snapshot = await discovery.DiscoverAsync(context.CancellationToken);
var state = Availability(snapshot);
var qualities = state == ProviderDownloadAvailabilityState.Available
? Enum.GetValues<ProviderAudioQuality>()
: [];
return ProviderOutcome<ProviderDownloadAvailability>.Success(new(state, qualities));
}
catch (OperationCanceledException)
{
return ProviderOutcome<ProviderDownloadAvailability>.Failure(new(ProviderErrorKind.Canceled));
}
catch
{
return ProviderOutcome<ProviderDownloadAvailability>.Failure(new(ProviderErrorKind.TransientFailure));
}
}
public async Task<ProviderOutcome<ProviderDownloadedArtifact>> DownloadAsync(
ProviderExecutionContext context,
ProviderDownloadRequest request,
IProgress<ProviderDownloadProgress>? progress = null)
{
var error = Validate(context, request.TrackId);
if (error != null) return ProviderOutcome<ProviderDownloadedArtifact>.Failure(error);
if (!context.Policy.AllowManagedDownloads)
return ProviderOutcome<ProviderDownloadedArtifact>.Failure(new(ProviderErrorKind.Forbidden));
if (request.DurableJobId == Guid.Empty)
return ProviderOutcome<ProviderDownloadedArtifact>.Failure(new(ProviderErrorKind.PermanentFailure));
try
{
progress?.Report(new(ProviderDownloadProgressStage.Resolving, 0));
var snapshot = await discovery.DiscoverAsync(context.CancellationToken);
var availability = Availability(snapshot);
if (availability != ProviderDownloadAvailabilityState.Available)
return ProviderOutcome<ProviderDownloadedArtifact>.Failure(new(ErrorFor(snapshot.State)));
if (!OutboundRequestGuard.TryCreateConfiguredServiceUri(settings.BaseUrl, out var baseUri, out _))
return ProviderOutcome<ProviderDownloadedArtifact>.Failure(new(ProviderErrorKind.AccountNeedsConfiguration));
var quality = Quality(request.RequestedQuality, settings.Quality);
var endpoint = new Uri(baseUri!, $"api/download/{Uri.EscapeDataString(request.TrackId.Value)}?quality={Uri.EscapeDataString(quality)}");
using var response = await http.GetAsync(endpoint, HttpCompletionOption.ResponseHeadersRead, context.CancellationToken);
if (response.StatusCode is >= HttpStatusCode.MultipleChoices and < HttpStatusCode.BadRequest ||
response.RequestMessage?.RequestUri is not { } responseUri || !SameOrigin(baseUri!, responseUri))
return ProviderOutcome<ProviderDownloadedArtifact>.Failure(new(ProviderErrorKind.PermanentFailure));
if (!response.IsSuccessStatusCode)
return ProviderOutcome<ProviderDownloadedArtifact>.Failure(ErrorFor(response));
var media = Media(response.Content.Headers.ContentType?.MediaType, quality);
if (media == null)
return ProviderOutcome<ProviderDownloadedArtifact>.Failure(new(ProviderErrorKind.IncompatibleMedia));
var expectedBytes = response.Content.Headers.ContentLength;
if (expectedBytes is <= 0 || expectedBytes > maximumArtifactBytes)
return ProviderOutcome<ProviderDownloadedArtifact>.Failure(new(ProviderErrorKind.IncompatibleMedia));
var artifactId = ArtifactId(request.TrackId.Value, media.Container);
await using var content = await response.Content.ReadAsStreamAsync(context.CancellationToken);
progress?.Report(new(ProviderDownloadProgressStage.Transferring, 0, expectedBytes));
var written = await artifacts.WriteAsync(new(
request.Workspace,
request.DurableJobId,
StableProviderId,
artifactId,
content,
maximumArtifactBytes)
{
ExpectedBytes = expectedBytes,
Progress = (complete, total) => progress?.Report(
new ProviderDownloadProgress(ProviderDownloadProgressStage.Transferring, complete, total))
}, context.CancellationToken);
progress?.Report(new(ProviderDownloadProgressStage.Verifying, written.SizeBytes, written.SizeBytes));
var output = new ProviderDownloadedArtifact(
written.ArtifactId,
written.Sha256,
written.SizeBytes,
media,
verified: true);
progress?.Report(new(ProviderDownloadProgressStage.Completed, written.SizeBytes, written.SizeBytes));
return ProviderOutcome<ProviderDownloadedArtifact>.Success(output);
}
catch (OperationCanceledException)
{
return ProviderOutcome<ProviderDownloadedArtifact>.Failure(new(ProviderErrorKind.Canceled));
}
catch (InvalidDataException)
{
return ProviderOutcome<ProviderDownloadedArtifact>.Failure(new(ProviderErrorKind.IncompatibleMedia));
}
catch (UnauthorizedAccessException)
{
return ProviderOutcome<ProviderDownloadedArtifact>.Failure(new(ProviderErrorKind.Forbidden));
}
catch (Exception exception) when (exception is HttpRequestException or IOException)
{
return ProviderOutcome<ProviderDownloadedArtifact>.Failure(new(ProviderErrorKind.TransientFailure));
}
catch
{
return ProviderOutcome<ProviderDownloadedArtifact>.Failure(new(ProviderErrorKind.PermanentFailure));
}
}
public static ProviderRegistration CreateRegistration(AppleDownloadCapabilityAdapter adapter) => new(
new ProviderDescriptor(
StableProviderId,
"Apple download",
"Optional operator-managed Apple audio downloads through a discovered compatible gateway.",
ProviderOrigin.BuiltIn,
sdkVersion: "1",
compatibilityVersion: "apple-download-gateway-v1",
capabilities:
[
ConfiguredLane(ProviderCapabilityKind.Metadata),
ConfiguredLane(ProviderCapabilityKind.Streaming),
new ProviderCapabilityDescriptor(
ProviderCapabilityKind.Download,
ProviderCapabilitySupportState.Supported,
ProviderAccountRequirement.None,
compatibilityVersion: "1",
hooks: ["checkAvailability", "download"]),
ConfiguredLane(ProviderCapabilityKind.Health)
],
permissions: new ProviderPermissionDescriptor()),
[adapter]);
private static ProviderCapabilityDescriptor ConfiguredLane(ProviderCapabilityKind capability) => new(
capability,
ProviderCapabilitySupportState.ConfiguredOnly,
ProviderAccountRequirement.None,
compatibilityVersion: "legacy-seam-v1");
private static ProviderError? Validate(ProviderExecutionContext context, ProviderExternalResourceId trackId)
{
ArgumentNullException.ThrowIfNull(context);
try
{
context.RequireResourceOwner(trackId, ProviderResourceKind.Track);
}
catch (Exception exception) when (exception is ArgumentException or UnauthorizedAccessException)
{
return new(ProviderErrorKind.Forbidden);
}
if (!context.ProviderId.Equals(StableProviderId, StringComparison.Ordinal) ||
!context.Policy.AllowsProvider(StableProviderId))
return new(ProviderErrorKind.Forbidden);
if (context.CancellationToken.IsCancellationRequested)
return new(ProviderErrorKind.Canceled);
return context.IsExpired(DateTimeOffset.UtcNow)
? new(ProviderErrorKind.CapabilityUnavailable)
: null;
}
private static ProviderDownloadAvailabilityState Availability(AppleDownloadEndpointSnapshot snapshot)
{
if (snapshot.State == AppleDownloadEndpointState.Available &&
snapshot.Capability(ProviderCapabilities.Download).State == AppleDownloadCapabilityState.Available &&
snapshot.Capability("download-audio-song").State == AppleDownloadCapabilityState.Available)
return ProviderDownloadAvailabilityState.Available;
return snapshot.State switch
{
AppleDownloadEndpointState.NeedsAuthentication => ProviderDownloadAvailabilityState.AccountRequired,
AppleDownloadEndpointState.Incompatible => ProviderDownloadAvailabilityState.Incompatible,
_ => ProviderDownloadAvailabilityState.Unavailable
};
}
private static ProviderErrorKind ErrorFor(AppleDownloadEndpointState state) => state switch
{
AppleDownloadEndpointState.NeedsConfiguration => ProviderErrorKind.AccountNeedsConfiguration,
AppleDownloadEndpointState.NeedsAuthentication => ProviderErrorKind.Unauthorized,
AppleDownloadEndpointState.Incompatible => ProviderErrorKind.NotSupported,
_ => ProviderErrorKind.CapabilityUnavailable
};
private static ProviderError ErrorFor(HttpResponseMessage response) => response.StatusCode switch
{
HttpStatusCode.Unauthorized => new(ProviderErrorKind.Unauthorized),
HttpStatusCode.Forbidden => new(ProviderErrorKind.Forbidden),
HttpStatusCode.NotFound => new(ProviderErrorKind.NotFound),
HttpStatusCode.TooManyRequests => new(ProviderErrorKind.RateLimited, RetryAfter(response)),
>= HttpStatusCode.InternalServerError => new(ProviderErrorKind.TransientFailure),
_ => new(ProviderErrorKind.PermanentFailure)
};
private static TimeSpan RetryAfter(HttpResponseMessage response) =>
response.Headers.RetryAfter?.Delta is { } delta && delta >= TimeSpan.Zero
? delta
: TimeSpan.FromSeconds(30);
private static string Quality(ProviderAudioQuality requested, string? configured) => requested switch
{
ProviderAudioQuality.Lossy => "aac-320",
ProviderAudioQuality.Lossless => configured?.StartsWith("alac-", StringComparison.OrdinalIgnoreCase) == true
? configured : "alac-16-44",
ProviderAudioQuality.HighResolution => configured is "alac-24-192" or "alac-24-96"
? configured : "alac-24-96",
_ => string.IsNullOrWhiteSpace(configured) ? "alac-16-44" : configured.Trim()
};
private static ProviderMediaFormat? Media(string? mimeType, string quality) => mimeType?.ToLowerInvariant() switch
{
"audio/flac" or "audio/x-flac" => new(mimeType.ToLowerInvariant(), "flac", "flac"),
"audio/mp4" or "audio/x-m4a" or "audio/m4a" => new(mimeType.ToLowerInvariant(), "m4a",
quality.StartsWith("aac-", StringComparison.OrdinalIgnoreCase) ? "aac" : "alac"),
"audio/aac" => new(mimeType.ToLowerInvariant(), "aac", "aac"),
_ => null
};
private static string ArtifactId(string trackId, string extension)
{
var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(trackId))).ToLowerInvariant();
return $"apple-{hash[..32]}.{extension}";
}
private static bool SameOrigin(Uri expected, Uri actual) =>
expected.Scheme.Equals(actual.Scheme, StringComparison.OrdinalIgnoreCase) &&
expected.Host.Equals(actual.Host, StringComparison.OrdinalIgnoreCase) &&
expected.Port == actual.Port;
}
@@ -0,0 +1,24 @@
using allstarr.Core.Capabilities;
namespace allstarr.Core.Providers.AppleDownload;
public static class AppleDownloadCapabilityRegistration
{
public static IServiceCollection AddAppleDownloadCapability(this IServiceCollection services)
{
services.AddHttpClient(AppleDownloadCapabilityAdapter.HttpClientName, client =>
client.Timeout = TimeSpan.FromMinutes(30))
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
AllowAutoRedirect = false,
UseCookies = false,
MaxConnectionsPerServer = 2,
PooledConnectionLifetime = TimeSpan.FromMinutes(2)
});
services.AddSingleton<AppleDownloadCapabilityAdapter>();
services.AddSingleton<ProviderRegistration>(provider =>
AppleDownloadCapabilityAdapter.CreateRegistration(
provider.GetRequiredService<AppleDownloadCapabilityAdapter>()));
return services;
}
}
@@ -18,14 +18,6 @@ public static class BuiltInProviderDescriptorCatalog
public static IReadOnlyList<ProviderRegistration> LegacyRegistrations { get; } =
[
Registration(
"apple-download",
"Apple download",
"The existing gamdl-backed Apple catalog and managed download lane.",
Optional(ProviderCapabilityKind.Metadata),
Required(ProviderCapabilityKind.Streaming),
Required(ProviderCapabilityKind.Download),
Required(ProviderCapabilityKind.Health)),
Registration(
"qobuz",
"Qobuz",
+3 -1
View File
@@ -25,6 +25,7 @@ using allstarr.Core.Operations;
using allstarr.Core.Providers.Deezer;
using allstarr.Core.Providers.Spotify;
using allstarr.Core.Providers.AppleMusicKit;
using allstarr.Core.Providers.AppleDownload;
using allstarr.Core.Providers;
using allstarr.Core.Protocols;
using allstarr.Core.Protocols.Jellyfin;
@@ -257,7 +258,7 @@ builder.Services.ConfigureAll<HttpClientFactoryOptions>(options =>
{
options.HttpMessageHandlerBuilderActions.Add(builder =>
{
if (builder.Name is "AppleDownloadDiscovery" or "AppleMusic")
if (builder.Name is "AppleDownloadDiscovery" or "AppleDownloadCapability" or "AppleMusic")
{
return;
}
@@ -416,6 +417,7 @@ builder.Services.AddSingleton<IConcreteMetadataService>(sp =>
builder.Services.AddDeezerMetadataCapability();
builder.Services.AddSpotifyPlaylistCapability();
builder.Services.AddAppleMusicKitPlaylistCapability();
builder.Services.AddAppleDownloadCapability();
builder.Services.AddLegacyBuiltInProviderDescriptors();
// 2. Concrete Download Services
+67
View File
@@ -0,0 +1,67 @@
services:
allstarr:
environment:
AppleDownload__BaseUrl: http://apple-gateway:8000
apple-gateway:
build:
context: ./sidecars/apple-gateway
image: ${APPLE_GATEWAY_IMAGE:-allstarr/apple-gateway:1.0.0}
restart: unless-stopped
depends_on:
apple-wrapper:
condition: service_started
environment:
APPLE_GATEWAY_WRAPPER_URL: http://apple-wrapper:80
APPLE_GATEWAY_WRAPPER_DECRYPT_HOST: apple-wrapper
APPLE_GATEWAY_WRAPPER_DECRYPT_PORT: 10020
APPLE_GATEWAY_STOREFRONT: ${APPLE_GATEWAY_STOREFRONT:-us}
APPLE_GATEWAY_MAX_CONCURRENCY: ${APPLE_GATEWAY_MAX_CONCURRENCY:-2}
APPLE_GATEWAY_PROCESS_TIMEOUT_SECONDS: ${APPLE_GATEWAY_PROCESS_TIMEOUT_SECONDS:-900}
volumes:
- apple-gateway-data:/data
networks:
- allstarr
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
tmpfs:
- /tmp:size=128m,mode=1777
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/api/health', timeout=3)"]
interval: 30s
timeout: 5s
retries: 5
start_period: 20s
apple-wrapper:
build:
context: ${APPLE_WRAPPER_CONTEXT:-./.apple-provider/wrapper-v2}
dockerfile: Dockerfile
args:
TARGET_ARCH: ${APPLE_WRAPPER_TARGET_ARCH:-x86_64}
BUILD_PLATFORM: linux/amd64
RUNTIME_PLATFORM: ${APPLE_WRAPPER_RUNTIME_PLATFORM:-linux/amd64}
image: ${APPLE_WRAPPER_IMAGE:-allstarr/wrapper-v2:0.0.2}
restart: unless-stopped
environment:
WRAPPER_HOST: 0.0.0.0
WRAPPER_PORT: 80
WRAPPER_DECRYPT_HOST: 0.0.0.0
WRAPPER_DECRYPT_PORT: 10020
WRAPPER_BASE_DIR: /data/data/com.apple.android.music/files
WRAPPER_RESTORE_SESSION: "1"
volumes:
- apple-wrapper-session:/app/rootfs/data/data/com.apple.android.music/files
networks:
- allstarr
cap_add:
- SYS_ADMIN
- SYS_CHROOT
- SYS_PTRACE
security_opt:
- apparmor:unconfined
volumes:
apple-gateway-data:
apple-wrapper-session:
+42 -26
View File
@@ -1,24 +1,47 @@
# External Apple download provider
# Apple download provider
Apple downloads are optional. Standard Compose and AIO do not include GAMDL, wrapper-v2, or an Apple provider
gateway. Allstarr connects to a separately deployed compatible gateway by URL.
Apple downloads are optional. Standard Compose and AIO do not start them. The optional
`docker-compose.apple.yml` profile adds Allstarr's Apple gateway, GAMDL 3.8.2, and a locally built, source-locked
wrapper-v2 0.0.2 service. It does not contain or download Apple code.
The URL must point to a gateway that wraps GAMDL and wrapper-v2 and implements the API contract Allstarr expects.
Do not enter the raw wrapper-v2 URL. wrapper-v2 supplies account, playback, and decryption services, but it is not a
GAMDL search and download HTTP gateway by itself.
## Prepare the external gateway
## Prepare wrapper-v2
Deploy the gateway in its own stack by following its maintainer's instructions. Keep its session state, downloaded
artifacts, backups, upgrades, and rollback outside the Allstarr stack. The upstream projects are
Obtain Apple Music for Android legally from a source you are permitted to use. Allstarr does not download,
redistribute, or retain that package. Then run:
```bash
./allstarr.sh prepare-apple /private/path/apple-music.apkm x86_64
./allstarr.sh up
```
Use `--apkm` for an APKM bundle. For ARM64 use `--arch arm64-v8a`, then set
`APPLE_WRAPPER_TARGET_ARCH=arm64-v8a` and `APPLE_WRAPPER_RUNTIME_PLATFORM=linux/arm64`.
An old installation's archived libraries can be reused only when they pass the official hash lock:
```bash
tools/apple-provider/prepare.sh --staged-libs /backup/rootfs/system/lib64 --arch x86_64
```
The controller clones official wrapper-v2 tag `0.0.2`, verifies its full locked commit, stages the official
AOSP runtime, and rejects Apple or AOSP libraries that do not match wrapper-v2's `LIBS_VERSION.json`. The generated
`.apple-provider/wrapper-v2` directory is local deployment input and must not be committed.
## What the profile runs
The repository gateway is a narrow HTTP adapter around the official upstream projects
[GAMDL](https://github.com/glomatico/gamdl) and
[wrapper-v2](https://github.com/glomatico/wrapper-v2). Neither project by itself implements the gateway contract
below. The gateway is the HTTP adapter between those tools and Allstarr.
[wrapper-v2](https://github.com/glomatico/wrapper-v2). It exposes catalog song lookup, download-backed streaming,
managed song downloads, health, login, and 2FA. It runs on the private Compose network and publishes no host port.
For a GAMDL-backed gateway, use GAMDL 3.8.2 or newer with wrapper-v2 0.0.2 or the exact compatible pair required by
the gateway. GAMDL 3.8.2 introduced compatibility with wrapper-v2 0.0.2. Do not update one half of the pair without
the other. The gateway's runtime capability and version manifest is authoritative; an upstream feature does not
become an Allstarr capability until the gateway advertises it and Allstarr's probe accepts it.
The gateway advertises only routes that Allstarr has implemented and tested. GAMDL can do more upstream, but album,
playlist, artist, library, video, lyrics-artifact, and artwork lanes stay unavailable in Allstarr until their
managed-artifact contracts exist. That keeps the UI honest and prevents an upstream feature name from becoming a
false promise.
Before connecting it, confirm that:
@@ -44,10 +67,8 @@ Allstarr first requests `GET /api/capabilities`. A compatible version 1 response
}
```
The gateway may additionally advertise `metadata-album`, `metadata-artist`, `download-album`,
`download-playlist`, `library-read`, `stream-music-video`, `synced-lyrics-artifact`, `tagging-artwork`,
`codec-alac`, and `codec-aac`. Allstarr displays absent features as unsupported. It never turns an upstream GAMDL
feature into an Allstarr route merely because GAMDL supports it.
The included gateway also advertises its verified ALAC and AAC input support. Allstarr displays absent features as
unsupported.
The current typed lanes use these routes:
@@ -66,12 +87,8 @@ an unexpected host.
## Connect Allstarr
Open the dashboard, find the Apple download provider, and set its gateway URL. This durable WebUI setting takes
effect without recreating Allstarr. A Compose deployment may instead supply the initial bootstrap value:
```dotenv
APPLE_DOWNLOAD_URL=https://apple-gateway.example.internal
```
Open the dashboard and find the Apple download provider. The Compose profile configures its private gateway URL.
Finish Apple login or 2FA there; credentials are forwarded only to wrapper-v2 and are not stored in Postgres.
The legacy `APPLE_MUSIC_AIO_URL` name is recognized only by the migration review so an operator can identify the
old endpoint. New deployments use `APPLE_DOWNLOAD_URL`.
@@ -94,10 +111,9 @@ access. It is not a GAMDL or wrapper account and must not be copied into the dow
## Disable or replace the gateway
Clear the configured URL in the dashboard. This disables only the Apple download provider and takes effect without
restarting Allstarr. It does not delete Postgres records, Allstarr-managed media, the external gateway's session
state, or a user's Apple MusicKit account. If the URL came from `APPLE_DOWNLOAD_URL`, remove that bootstrap value
and recreate only the Allstarr container.
Run `./allstarr.sh disable apple` followed by `./allstarr.sh up`. This removes the optional containers from the
active profile. It does not delete Postgres records, Allstarr-managed media, the wrapper session volume, gateway
state, or a user's Apple MusicKit account.
To replace the gateway, validate the replacement independently, change the URL in the dashboard, and repeat the
provider health and capability checks. Do not combine that change with a database restore or media move. Keeping
+88
View File
@@ -0,0 +1,88 @@
# Deployment profiles
`allstarr.sh` is the normal Compose entry point. It keeps Standard small, remembers optional services, validates the
merged Compose model before starting it, and never deletes volumes.
## First install
```bash
./allstarr.sh init
```
This copies `.env.example` when needed, creates the media directories, and generates the Postgres password and
Allstarr encryption key ring with owner-only permissions. Edit `.env`, then start the core stack:
```bash
./allstarr.sh up
```
## Optional services
Spotify lyrics needs only its cookie in the protected `.env`:
```bash
./allstarr.sh enable spotify
./allstarr.sh up
```
Apple needs a legally obtained Apple Music Android 3.6.0-beta build 1109 APK/APKM. Allstarr verifies every native
library against the official wrapper-v2 lock before it will build:
```bash
./allstarr.sh prepare-apple /private/path/apple-music.apkm x86_64
./allstarr.sh up
```
Use `arm64-v8a` instead of `x86_64` on an ARM64 Docker host. Finish Apple login and 2FA in the WebUI. The Apple
download login is separate from every user's Apple MusicKit account.
The AIO override remains an offline first-party extension bundle. It is not an everything-in-one image and does not
quietly enable provider accounts or sidecars.
```bash
./allstarr.sh enable aio
./allstarr.sh up
```
## Updates
```bash
./allstarr.sh update
```
The command pulls reviewed images, rebuilds local Apple components only when that profile is enabled, recreates the
saved profile, and shows the resulting containers. It does not run `git pull`; source updates are an explicit
operator action. It does not remove Postgres, Valkey, Allstarr state, media, or provider-session volumes.
Use `./allstarr.sh status` to see the active profile and `./allstarr.sh logs SERVICE` for a bounded starting log
window followed by new events.
## Removing and re-adding a service
```bash
./allstarr.sh disable spotify
./allstarr.sh disable apple
./allstarr.sh up
```
Disabling a profile removes its containers on the next `up` but keeps its volumes and durable Allstarr settings.
Re-enable Spotify with `enable spotify`; re-enable Apple by running `prepare-apple` again or adding `apple` to the
local `.allstarr-profiles` file after the locked wrapper context is present.
## Adding another sidecar
Keep custom services in a separate Compose override. Give the service a fixed image digest or locked build source,
join the existing `allstarr` network, publish no host ports unless an operator truly needs them, persist only the
state it owns, and point Allstarr at its private service URL. Do not mount the Docker socket into Allstarr or a
provider. Do not put provider source pulls in container startup.
Before using a new override:
```bash
docker compose -f docker-compose.yml -f docker-compose.my-provider.yml config --quiet
docker compose -f docker-compose.yml -f docker-compose.my-provider.yml up -d
```
An extension package is different from a sidecar. Provider SDK packages are installed through the WebUI registry
flow, where checksum, manifest, permissions, settings, health, activation, rollback, and uninstall state are
reviewed. See [Extension SDK v1](../extensions/sdk-v1.md).
+3 -2
View File
@@ -59,8 +59,9 @@ Typed routes must register an accurate descriptor and matching capability implem
### Apple
- `apple-download` is an optional, separately deployed compatible gateway configured by URL. Standard and AIO do
not bundle GAMDL, wrapper-v2, or the gateway stack.
- `apple-download` is an optional repository gateway and typed managed-download adapter. Its separate Compose
profile uses GAMDL 3.8.2 and locked wrapper-v2 0.0.2 source after legal Apple libraries pass upstream hashes.
Standard and AIO do not start that stack.
- `apple-musickit` is a separate per-user MusicKit account for personal playlists and library operations.
- Never pass a Music User Token to the download gateway or treat gateway login state as MusicKit authorization.
+2 -2
View File
@@ -39,7 +39,7 @@ only where it explains why a compatibility path still exists.
| Provider enablement and runtime status: [ProviderStatusManager.cs](../../../allstarr/Services/Common/ProviderStatusManager.cs), [ProviderRuntimeStatus.cs](../../../allstarr/Services/Common/ProviderRuntimeStatus.cs) | The compatibility projection separates configured state from in-memory `Unknown`, `Testing`, `Healthy`, or `Degraded` observations. Typed routes use durable account/capability samples, rollups, circuits, and resolved accounts. Disabled providers leave every lane, and SquidWTF is metadata-only. | **Keep the compatibility projection; use durable health and account policy for typed routes.** | `ProviderStatusManagerTests` covers pure compatibility reads and probe isolation. `Unknown` may be attempted only on eligible compatibility paths and is never reported ready. |
| Built-in provider adapters: [Services/AppleMusic](../../../allstarr/Services/AppleMusic), [Services/Deezer](../../../allstarr/Services/Deezer), [Services/Qobuz](../../../allstarr/Services/Qobuz), [Services/SquidWTF](../../../allstarr/Services/SquidWTF) | Current services expose uneven metadata, download-backed streaming, download, playlist-read, and health behavior through global configuration. | **Wrap individually.** Publish only capabilities listed by `CurrentProviderSupportCatalog`; replace internals only behind common typed contracts. | Provider-specific fake HTTP tests are rollback anchors. Missing download credentials must not suppress public metadata. A capability must not inherit another capability's failed probe. |
| SquidWTF stream/download paths | Legacy download/stream classes remain present, but current status/routing excludes SquidWTF from stream, download, and playlist lanes. | **Retire from current routing; retain metadata adapter.** Reintroduce another capability only with a working endpoint and contract fixture. | `ProviderStatusManagerTests` guards the quarantine. The optional uptime source is discovery information, not proof of a working media endpoint. |
| External Apple download gateway: [AppleMusicController.cs](../../../allstarr/Controllers/AppleMusicController.cs), [Services/AppleMusic](../../../allstarr/Services/AppleMusic) | The admin API calls one configured gateway URL, understands nested account state, preserves 2FA/pending statuses, and strips tokens. Current media support is song-focused. | **Keep the HTTP boundary; retire the vendored deployment.** Require a compatible external gateway and advertise only capabilities its contract and health probe verify. | `AppleMusicControllerTests` protects status/login/secret shaping. Fake-gateway tests remain the rollback anchor. A raw wrapper-v2 URL is not a compatible search/download gateway. |
| Optional Apple download gateway: [AppleMusicController.cs](../../../allstarr/Controllers/AppleMusicController.cs), [Apple typed adapter](../../../allstarr/Core/Providers/AppleDownload), [gateway](../../../sidecars/apple-gateway) | The admin API calls the private optional gateway, preserves 2FA/pending statuses, and strips tokens. The typed adapter verifies song artifacts in host-owned workspaces. | **Keep the HTTP boundary and optional profile.** Advertise only capabilities its contract and health probe verify. | Controller and fake-gateway tests protect login, discovery, streamed artifact, MIME, size, checksum, and ownership behavior. |
| Playlist and matching stack: [Core/Playlists](../../../allstarr/Core/Playlists), [Core/Matching](../../../allstarr/Core/Matching), plus legacy [Services/Spotify](../../../allstarr/Services/Spotify) and [JellyfinController.Spotify.cs](../../../allstarr/Controllers/JellyfinController.Spotify.cs) | New Spotify and Apple MusicKit sources are account-bound and provider-neutral. Durable snapshots, matches, virtual reads, schedules, and Jellyfin/Subsonic materialization use the core. The specialized Redis mapping/injection path remains compatibility-only. | **Keep the provider-neutral core; retire legacy compatibility after its fixtures and routes are removed deliberately.** | Playlist orchestration, target, virtual protocol, scanner, tenant-isolation, WebUI, and mapping-review tests cover the new path. Fresh installs do not import legacy cache mappings. |
| Lyrics, playback, and scrobbling: [Services/Lyrics](../../../allstarr/Services/Lyrics), [Services/Scrobbling](../../../allstarr/Services/Scrobbling), [Core/Playback](../../../allstarr/Core/Playback) | Lyrics sources remain orchestrated services. Authenticated playback observations and optional Last.fm or ListenBrainz delivery now cross a durable, idempotent job boundary with exact actor scope. | **Keep the provider services behind the durable core.** Preserve native protocol results, dedupe, scoped credentials, retention, and sanitized failures. Do not add controller fire-and-forget work. | Playback pipeline, scrobbling orchestrator, protocol, and admin tests cover thresholds, retries, scope, and failure shaping. Do not log raw upstream bodies or tokens. |
| Recommendations and generated sets: [Core/Intelligence](../../../allstarr/Core/Intelligence), [IntelligenceController.cs](../../../allstarr/Controllers/IntelligenceController.cs) | Exact-scope policy controls retained signals, habit profiles, provider readiness, recommendation runs, explanations, and generated playlists. Jellyfin and Subsonic materializers reconcile exact local matches through shared playlist targets. | **Extend through typed sources and targets.** Keep MusicBrainz claims limited to local enriched relationships, require explicit Subsonic credentials, and treat AudioMuse as optional until healthy and contract-compatible. | Intelligence, provider-source, controller, and generated-set materializer tests cover opt-in, purge, seeds, isolation, ordering, skips, retries, and cancellation. |
@@ -51,7 +51,7 @@ only where it explains why a compatibility path still exists.
| Cache and current runtime persistence: [RedisCacheService.cs](../../../allstarr/Services/Common/RedisCacheService.cs), [RedisPersistenceService.cs](../../../allstarr/Services/Common/RedisPersistenceService.cs), `.env`, `/app/cache` | Redis/Valkey, cache snapshots, JSON files, and `.env` hold current cache, mapping, session, extension, and configuration state. They are not a transactional job/account store. | **Retire legacy durable ownership; keep only cache-compatible seams where still needed.** | The overhaul uses a fresh database and configuration, so legacy cache and runtime files are not backfilled. Never silently infer SQLite or treat Redis/Valkey snapshots as the durable outbox. |
| Hosted maintenance and background work: [Program.cs](../../../allstarr/Program.cs), [Services/Common](../../../allstarr/Services/Common), [Services/Spotify](../../../allstarr/Services/Spotify), [Core](../../../allstarr/Core) | Startup validation, cache warming/cleanup/persistence, Spotify compatibility fetch/match, durable jobs/outbox, favorite actions, library refresh, provider health, and sidecar monitoring run in process. Automatic pre-overhaul environment, favorite, mapping, and version-state migration services were retired. Optional provider startup probes are off by default and live validators do not run in `Testing`. | **Keep mutations leased, durable, scoped, and idempotent.** Keep the fresh-install boundary instead of adding startup imports. | Host and worker tests use temporary state and fake HTTP so CI never contacts live providers or backend libraries. |
| WebUI and schema: [wwwroot](../../../allstarr/wwwroot), [AdminUiSchema.cs](../../../allstarr/Models/Admin/AdminUiSchema.cs), [CurrentProviderSupportCatalog.cs](../../../allstarr/Services/Common/CurrentProviderSupportCatalog.cs) | The Lit UI consumes an admin schema and shows the current provider support matrix, distinct configuration/observed-health states, compact support chips, responsive grids/tables, and keyboard-operable mobile navigation. | **Refactor in place.** Keep current workflows; add sections only with durable APIs and complete states. | `JavaScriptSyntaxTests`, `CurrentProviderSupportCatalogTests`, and `WebUiResponsiveContractTests` are static regression anchors. Browser functional, focus, narrow-screen, and accessibility validation remain release checks. |
| Compose and optional services: [docker-compose.yml](../../../docker-compose.yml), [docker-compose.aio.yml](../../../docker-compose.aio.yml) | The fresh standard stack is core/Postgres/Valkey. AIO mounts only verified first-party packages. Apple downloads require an operator-owned compatible gateway configured by URL. | **Keep the shipped stack small.** Do not vendor or publish the Apple gateway from this repository. Optional endpoints need an explicit contract, health gate, and resource boundary. | Compose contract tests validate Standard and AIO. External-provider contract tests validate gateway behavior without a live account. Never hide a storage-provider change in an override. |
| Compose and optional services: [docker-compose.yml](../../../docker-compose.yml), [docker-compose.aio.yml](../../../docker-compose.aio.yml), [docker-compose.apple.yml](../../../docker-compose.apple.yml) | The fresh standard stack is core/Postgres/Valkey. AIO mounts only verified first-party packages. Apple remains an explicit source-locked profile requiring operator-supplied legal libraries. | **Keep the default stack small and optional profiles turnkey.** Optional endpoints need an explicit contract, health gate, and resource boundary. | Compose and gateway contract tests validate the profiles without a live account. Never hide a storage-provider change in an override. |
| Test and fixture infrastructure: [allstarr.Tests](../../../allstarr.Tests), [Fixtures/Protocols](../../../allstarr.Tests/Fixtures/Protocols) | xUnit covers current helpers, services, controllers, real-host composition, protocol source locks/support matrix, route/auth fixtures, provider status, extensions, redaction, file safety, and UI contracts. | **Keep and expand.** Fixtures are compatibility inputs, not generated promises. | `dotnet test allstarr.sln` is the minimum gate. No automated test may require a live provider account, backend library, or provider traffic. |
## Source And Fixture Anchors
@@ -247,9 +247,9 @@ Uninstall requires explicit confirmation. It disables the extension first, remov
Apple is represented by two separate provider instances:
- `apple-download`: download and optional stream behavior exposed by a separately deployed compatible provider
gateway. A GAMDL-backed gateway can create managed song, music-video, synced-lyrics, cover-art, and rich-tagging
artifacts where its account, source, codec, and advertised contract allow it.
- `apple-download`: download and download-backed stream behavior exposed by the optional repository gateway. The
typed lane currently accepts verified song audio only; broader GAMDL outputs remain unavailable until their
managed-artifact contracts and tests exist.
- `apple-musickit`: per-user MusicKit API access to personal-library playlists and playlist items, library songs/albums/artists, and documented library or favorite-state actions.
References:
@@ -270,9 +270,9 @@ Implementation notes:
- `apple-musickit` is normally per-user because playlists, library songs, and liked/library state belong to the signed-in Apple Music user.
- gamdl supports catalog and library song, album, playlist, artist, and music-video URLs. Its download provider should preserve the media kind, chosen codec/quality, source URL identity, and output artifact metadata in the managed-download record.
- gamdl can output synced lyrics and rich tags with a managed download. Ingest that output after validation; expose it through the lyrics lane only when format, ownership, and routing policy permit. This is not a claim of generic on-demand lyric availability for every Apple track.
- Allstarr does not ship the gateway. The configured URL must point to an Allstarr-compatible HTTP gateway, not
directly to wrapper-v2. wrapper-v2 has account, playback, and decryption endpoints but is not a GAMDL search and
download gateway by itself.
- The optional profile ships the HTTP gateway but not Apple libraries. Its configured URL points to that gateway,
never directly to wrapper-v2. wrapper-v2 has account, playback, and decryption endpoints but is not a GAMDL
search and download gateway by itself.
- The gateway login UI must call its status endpoint before showing configured and must surface pending 2FA.
- If the gateway has session files but no usable token, show Needs Config.
- Apple lyric availability exists in public APIs, but full lyric retrieval should be optional sidecar or extension behavior.
@@ -288,26 +288,26 @@ upstream GAMDL support from a gateway route that Allstarr has actually implement
| Rich tags, artwork, and downloaded-file metadata | `apple-download` artifact metadata plus enrichment | Upstream capability; preserve only verified output fields. | Artifact parser/validation and metadata-merge tests. |
| Synced LRC/SRT/TTML lyrics | Download artifact, then optional `ILyricsProvider` source | Not advertised without a compatible artifact route. | Explicit gateway output, format/ownership validation, and lyrics response tests. |
| Music videos up to available upstream quality | Download media of kind `music-video`; protocol exposure only where the selected backend/client supports it | No verified gateway video contract yet. | Media-kind contract, video route/job, codec/container validation, and protocol fixtures. |
| Wrapper-backed playback/decryption and high-quality codecs | `apple-download` stream/download policy | External gateway contract required. | Account-scoped health, codec fallback, and restart/recovery tests. |
| Wrapper-backed playback/decryption and high-quality codecs | `apple-download` stream/download policy | Included gateway uses the raw TCP wrapper protocol and advertises verified codecs. | Live account checks remain manual, opt-in, and redacted. |
Every provider descriptor must maintain the same kind of feature-coverage record: upstream capability, Allstarr capability mapping, current implementation state, configuration/account prerequisites, protocol limitations, and contract-test location. A feature may be marked deferred or unsupported only with a visible reason; it must never be silently omitted or advertised before it works.
GAMDL 3.8.2 introduced compatibility with wrapper-v2 0.0.2 and its raw TCP decrypt protocol. A compatible
external gateway should use GAMDL 3.8.2 or newer with wrapper-v2 0.0.2, or the exact pair required by its runtime
manifest. Treat them as one compatibility pair. Allstarr's repository does not vendor, build, publish, or update
that pair.
GAMDL 3.8.2 introduced compatibility with wrapper-v2 0.0.2 and its raw TCP decrypt protocol. The optional profile
locks that exact pair and passes wrapper HTTP and TCP endpoints separately. Treat them as one compatibility pair.
The repository never contains or downloads Apple's native libraries.
### External Gateway Update Policy
### Apple Profile Update Policy
The gateway operator owns upstream selection, image provenance, session compatibility, upgrade testing, and
rollback. Allstarr owns only its endpoint contract and capability probe.
The source lock owns upstream selection. An update changes GAMDL and wrapper-v2 together, then passes gateway,
Compose, architecture, and fake-account contract tests before release. The persistent wrapper session volume is
not replaced during a normal update.
- Configure only an explicit gateway URL. Never discover or pull provider code during Allstarr startup.
- Never discover or pull provider code during Allstarr startup. Preparation is an explicit operator command.
- Probe the API version, authentication state, health, and capability manifest before activation and after a URL
change.
- Use fake-gateway contract tests in CI. Automated tests must not require a live Apple account.
- Replace or remove a gateway without deleting provider records, Allstarr-managed media, or MusicKit accounts.
- Follow [the external Apple download runbook](../../operations/apple-download-provider.md) for setup and cutover.
- Follow [the Apple download runbook](../../operations/apple-download-provider.md) for setup and cutover.
## Other Built-In Providers
@@ -115,8 +115,9 @@ Possible later repository profiles remain separate work:
- `docker-compose.lyrics.yml`: lyrics sidecars only.
- `docker-compose.lowram.yml`: lower memory settings and fewer services.
Allstarr does not ship or maintain a GAMDL Compose overlay. An operator may run a compatible Apple provider
gateway in another stack and configure its URL. Standard and AIO remain complete without it.
`docker-compose.apple.yml` is an explicit optional profile. It builds the repository gateway and the locked
wrapper-v2 source only after the operator supplies hash-verified legal Apple libraries. Standard and AIO remain
complete without it, and removing the profile preserves Postgres, media, gateway state, and wrapper login state.
Compose-file selection and any Compose profiles are explicit operator inputs. Deployment documentation should show the selected files/profile, database choice, persistent volume locations, and the rendered `docker compose config` output before an upgrade. Do not infer AIO or low-RAM mode from available memory, and do not hide a storage-provider change inside an override file.
+25
View File
@@ -0,0 +1,25 @@
FROM python:3.12.11-slim-bookworm@sha256:519591d6871b7bc437060736b9f7456b8731f1499a57e22e6c285135ae657bf7
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
APPLE_GATEWAY_DATA_ROOT=/data \
PATH=/app/.venv/bin:$PATH
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates ffmpeg \
&& rm -rf /var/lib/apt/lists/* \
&& useradd --system --uid 10001 --home-dir /nonexistent --shell /usr/sbin/nologin gateway
WORKDIR /app
COPY pyproject.toml uv.lock README.md /app/
COPY apple_gateway /app/apple_gateway
RUN pip install --no-cache-dir uv==0.11.29 \
&& uv sync --frozen --no-dev --no-editable \
&& pip uninstall -y uv
RUN install -d -o gateway -g gateway /data
USER gateway
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/api/health', timeout=3)"
CMD ["uvicorn", "apple_gateway.app:app", "--host", "0.0.0.0", "--port", "8000", "--no-access-log"]
+54
View File
@@ -0,0 +1,54 @@
# Allstarr Apple download gateway
This optional service implements Allstarr's Apple download gateway API. It installs the official `gamdl` 3.8.2
Python package and calls its documented CLI. It does not contain wrapper-v2 source, Apple native libraries,
credentials, or session data.
The optional root Compose profile builds the locked official wrapper-v2 0.0.2 source beside this gateway and puts
both on Allstarr's private network. Do not expose the gateway or wrapper login endpoints to the public Internet.
## Runtime contract
- `GET /api/capabilities`
- `GET /api/health`
- `GET /api/me`
- `POST /api/login`
- `POST /api/login/2fa`
- `GET /api/search?q=...&type=song&limit=...`
- `GET /api/song/{songId}`
- `GET /api/download/{songId}?quality=...`
- `GET /api/stream/{songId}?quality=...`
- `POST /api/jobs/download` for supported Apple catalog or library URLs
- `GET /api/jobs/download/{jobId}`
Song downloads return a FLAC artifact because that is the current Allstarr managed-song contract. Generic jobs
preserve GAMDL's own media, artwork, tag, and synced-lyrics outputs in the gateway data volume.
## Configuration
| Variable | Default | Purpose |
| --- | --- | --- |
| `APPLE_GATEWAY_WRAPPER_URL` | `http://wrapper-v2` | Separate wrapper-v2 origin |
| `APPLE_GATEWAY_WRAPPER_DECRYPT_HOST` | `wrapper-v2` | Wrapper-v2 raw decrypt host reachable from this container |
| `APPLE_GATEWAY_WRAPPER_DECRYPT_PORT` | `10020` | Wrapper-v2 raw decrypt port reachable from this container |
| `APPLE_GATEWAY_STOREFRONT` | `us` | Two-letter Apple storefront |
| `APPLE_GATEWAY_DATA_ROOT` | `/data` | Persistent artifact and temporary root |
| `APPLE_GATEWAY_COOKIES_PATH` | empty | Optional cookies file used by GAMDL |
| `APPLE_GATEWAY_MAX_CONCURRENCY` | `2` | Shared GAMDL/FFmpeg subprocess limit |
| `APPLE_GATEWAY_PROCESS_TIMEOUT_SECONDS` | `900` | Hard subprocess deadline |
| `APPLE_GATEWAY_WRAPPER_TIMEOUT_SECONDS` | `45` | Wrapper request deadline |
| `APPLE_GATEWAY_MAX_PROCESS_OUTPUT_BYTES` | `32768` | Maximum retained stdout and stderr per process |
Prepare wrapper-v2 with the operator's legal APK/APKM, then use the saved Apple profile:
```bash
./allstarr.sh prepare-apple /private/path/apple-music.apkm x86_64
./allstarr.sh up
```
The root overlay configures `http://apple-gateway:8000` internally. Removing the profile does not change
Allstarr's Postgres state, existing media, gateway state, or wrapper login session.
The supported GAMDL URL kinds are songs, albums, playlists, artists, music videos, posts, and Apple library URLs.
The gateway passes arguments as an exec array, never through a shell. It rejects non-Apple hosts, URL credentials,
fragments, unsafe artifact paths, symlinks, redirects from wrapper-v2, and unbounded process output.
@@ -0,0 +1 @@
"""Allstarr's optional external Apple download gateway."""
+208
View File
@@ -0,0 +1,208 @@
from __future__ import annotations
import importlib.metadata
import shutil
import uuid
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Any
import httpx
from fastapi import FastAPI, HTTPException, Query
from fastapi.responses import FileResponse, JSONResponse
from starlette.background import BackgroundTask
from .catalog import CatalogClient
from .config import Settings
from .jobs import DownloadJobManager
from .models import DownloadJobRequest, DownloadJobView, Login2faRequest, LoginRequest
from .runner import BoundedProcessRunner, ProcessFailure
from .security import safe_files, song_url
from .wrapper import WrapperClient, WrapperResponse
API_VERSION = "1.0.0"
CAPABILITIES = (
"metadata-search-song",
"metadata-song",
"stream-audio-song",
"download-audio-song",
"codec-alac",
"codec-aac",
)
def _version(distribution: str) -> str:
try:
return importlib.metadata.version(distribution)
except importlib.metadata.PackageNotFoundError:
return "unavailable"
def _authenticated(payload: Any) -> bool:
if not isinstance(payload, dict):
return False
if payload.get("logged_in") is True or payload.get("authenticated") is True:
return True
auth = payload.get("auth")
return isinstance(auth, dict) and (
auth.get("logged_in") is True
or str(auth.get("state", "")).lower() in {"authenticated", "logged_in", "ready"}
)
def _codec(quality: str) -> str:
mapping = {
"alac": "alac",
"alac-16-44": "alac",
"alac-24-96": "alac",
"alac-24-192": "alac",
"aac": "aac",
"aac-320": "aac",
"aac-web": "aac-web",
"aac-96": "aac-he",
"aac-he": "aac-he",
"aac-he-web": "aac-he-web",
}
try:
return mapping[quality.lower()]
except KeyError as exc:
raise HTTPException(status_code=400, detail="unsupported_quality") from exc
def _forward(response: WrapperResponse) -> JSONResponse:
return JSONResponse(status_code=response.status_code, content=response.payload)
def create_app(
settings: Settings | None = None,
wrapper: WrapperClient | None = None,
catalog: CatalogClient | None = None,
runner: BoundedProcessRunner | None = None,
) -> FastAPI:
config = settings or Settings.from_env()
wrapper_client = wrapper or WrapperClient(config.wrapper_url, config.wrapper_timeout_seconds)
catalog_client = catalog or CatalogClient(config.storefront)
process_runner = runner or BoundedProcessRunner(config)
jobs = DownloadJobManager(process_runner, config.data_root)
@asynccontextmanager
async def lifespan(_: FastAPI):
config.prepare()
yield
await jobs.close()
await wrapper_client.close()
await catalog_client.close()
application = FastAPI(
title="Allstarr Apple download gateway",
version=API_VERSION,
docs_url=None,
redoc_url=None,
lifespan=lifespan,
)
@application.get("/api/capabilities")
async def capabilities() -> dict[str, Any]:
return {
"sidecarApiVersion": API_VERSION,
"runtime": {"gateway": API_VERSION, "gamdl": _version("gamdl")},
"capabilities": [{"id": item, "state": "supported"} for item in CAPABILITIES],
}
@application.get("/api/health")
async def health() -> dict[str, Any]:
wrapper_health = await wrapper_client.health()
me = await wrapper_client.me() if wrapper_health.status_code == 200 else WrapperResponse(503, {})
wrapper_payload = wrapper_health.payload if isinstance(wrapper_health.payload, dict) else {}
wrapper_version = str(wrapper_payload.get("version") or "unknown")
executable = shutil.which(config.gamdl_path) is not None
return {
"status": "ok" if executable and wrapper_health.status_code == 200 else "degraded",
"staged": executable,
"daemon_running": wrapper_health.status_code == 200,
"wrapper_healthy": wrapper_health.status_code == 200,
"logged_in": _authenticated(me.payload),
"versions": {"gateway": API_VERSION, "gamdl": _version("gamdl"), "wrapper": wrapper_version},
}
@application.get("/api/me")
async def me() -> JSONResponse:
return _forward(await wrapper_client.me())
@application.post("/api/login")
async def login(request: LoginRequest) -> JSONResponse:
return _forward(await wrapper_client.login(request.username, request.password))
@application.post("/api/login/2fa")
async def login_2fa(request: Login2faRequest) -> JSONResponse:
return _forward(await wrapper_client.login_2fa(request.code))
@application.get("/api/search")
async def search(
q: str = Query(min_length=1, max_length=500),
type: str = Query(default="song", pattern="^song$"),
limit: int = Query(default=20, ge=1, le=100),
) -> list[dict[str, Any]]:
del type
try:
return await catalog_client.search_songs(q, limit)
except (httpx.HTTPError, ValueError):
raise HTTPException(status_code=502, detail="catalog_unavailable") from None
@application.get("/api/song/{song_id}")
async def song(song_id: str) -> dict[str, Any]:
try:
result = await catalog_client.song(song_id)
except (httpx.HTTPError, ValueError):
raise HTTPException(status_code=502, detail="catalog_unavailable") from None
if result is None:
raise HTTPException(status_code=404, detail="song_not_found")
return result
@application.get("/api/download/{song_id}")
@application.get("/api/stream/{song_id}")
async def download_song(song_id: str, quality: str = "alac-16-44") -> FileResponse:
try:
url = song_url(config.storefront, song_id)
except ValueError:
raise HTTPException(status_code=400, detail="invalid_song_id") from None
request_id = uuid.uuid4().hex
root = config.data_root / "artifacts" / request_id
try:
artifacts = await process_runner.download(url, _codec(quality), root / "output", root / "temporary")
audio = safe_files(root / "output", {".m4a", ".flac"})
if not audio:
raise ProcessFailure("audio_artifact_missing")
artifact = await process_runner.to_flac(audio[0], root / f"{song_id}.flac")
except ProcessFailure as exc:
shutil.rmtree(root, ignore_errors=True)
status = 504 if exc.code == "process_timeout" else 502
raise HTTPException(status_code=status, detail=exc.code) from None
except Exception:
shutil.rmtree(root, ignore_errors=True)
raise HTTPException(status_code=502, detail="download_failed") from None
return FileResponse(
artifact,
media_type="audio/flac",
filename=f"{song_id}.flac",
background=BackgroundTask(shutil.rmtree, root, ignore_errors=True),
)
@application.post("/api/jobs/download", status_code=202, response_model=DownloadJobView)
async def enqueue_download(request: DownloadJobRequest) -> DownloadJobView:
try:
return jobs.enqueue(request.url, _codec(request.quality))
except ValueError:
raise HTTPException(status_code=400, detail="unsupported_apple_music_url") from None
@application.get("/api/jobs/download/{job_id}", response_model=DownloadJobView)
async def get_download_job(job_id: str) -> DownloadJobView:
job = jobs.get(job_id)
if job is None:
raise HTTPException(status_code=404, detail="job_not_found")
return job
return application
app = create_app()
@@ -0,0 +1,60 @@
from __future__ import annotations
from typing import Any
import httpx
class CatalogClient:
"""Small metadata seam backed by Apple's public iTunes Search API."""
def __init__(self, storefront: str, client: httpx.AsyncClient | None = None):
self._storefront = storefront.upper()
self._owns_client = client is None
self._client = client or httpx.AsyncClient(
base_url="https://itunes.apple.com/",
timeout=httpx.Timeout(10),
follow_redirects=False,
trust_env=False,
)
async def close(self) -> None:
if self._owns_client:
await self._client.aclose()
async def search_songs(self, query: str, limit: int) -> list[dict[str, Any]]:
response = await self._client.get("search", params={
"term": query,
"country": self._storefront,
"media": "music",
"entity": "song",
"limit": limit,
})
response.raise_for_status()
return [self._map(item) for item in response.json().get("results", []) if item.get("trackId")]
async def song(self, song_id: str) -> dict[str, Any] | None:
response = await self._client.get("lookup", params={"id": song_id, "country": self._storefront})
response.raise_for_status()
item = next((item for item in response.json().get("results", []) if item.get("trackId")), None)
return self._map(item) if item else None
@staticmethod
def _map(item: dict[str, Any]) -> dict[str, Any]:
artwork = str(item.get("artworkUrl100") or "").replace("100x100", "1200x1200")
release = item.get("releaseDate")
return {
"id": str(item["trackId"]),
"title": str(item.get("trackName") or "Unknown title"),
"artist": str(item.get("artistName") or "Unknown artist"),
"album": str(item.get("collectionName") or "Unknown album"),
"duration": max(0, int(item.get("trackTimeMillis") or 0) // 1000),
"cover_url": artwork,
"track_number": item.get("trackNumber"),
"disc_number": item.get("discNumber"),
"isrc": item.get("isrc"),
"release_date": release[:10] if isinstance(release, str) and len(release) >= 10 else None,
"copyright": item.get("copyright"),
"composer": None,
"genre": item.get("primaryGenreName"),
}
@@ -0,0 +1,77 @@
from __future__ import annotations
import os
from dataclasses import dataclass
from pathlib import Path
from urllib.parse import urlsplit
def _positive_int(name: str, default: int, minimum: int, maximum: int) -> int:
raw = os.getenv(name, str(default))
try:
value = int(raw)
except ValueError as exc:
raise RuntimeError(f"{name} must be an integer") from exc
if not minimum <= value <= maximum:
raise RuntimeError(f"{name} must be between {minimum} and {maximum}")
return value
def _http_url(name: str, default: str) -> str:
value = os.getenv(name, default).rstrip("/")
parsed = urlsplit(value)
if parsed.scheme not in {"http", "https"} or not parsed.hostname or parsed.username or parsed.password:
raise RuntimeError(f"{name} must be an HTTP(S) origin without credentials")
if parsed.path not in {"", "/"} or parsed.query or parsed.fragment:
raise RuntimeError(f"{name} must not contain a path, query, or fragment")
return value
def _host(name: str, default: str) -> str:
value = os.getenv(name, default).strip()
if not value or any(character in value for character in "/:@?#[]"):
raise RuntimeError(f"{name} must be a hostname or address without a port")
return value
@dataclass(frozen=True, slots=True)
class Settings:
wrapper_url: str
wrapper_decrypt_host: str
wrapper_decrypt_port: int
data_root: Path
cookies_path: Path | None
storefront: str
gamdl_path: str
ffmpeg_path: str
subprocess_timeout_seconds: int
wrapper_timeout_seconds: int
max_concurrency: int
max_process_output_bytes: int
@classmethod
def from_env(cls) -> "Settings":
root = Path(os.getenv("APPLE_GATEWAY_DATA_ROOT", "/data")).expanduser().resolve()
cookies = os.getenv("APPLE_GATEWAY_COOKIES_PATH", "").strip()
storefront = os.getenv("APPLE_GATEWAY_STOREFRONT", "us").strip().lower()
if len(storefront) != 2 or not storefront.isascii() or not storefront.isalpha():
raise RuntimeError("APPLE_GATEWAY_STOREFRONT must be a two-letter storefront")
return cls(
wrapper_url=_http_url("APPLE_GATEWAY_WRAPPER_URL", "http://wrapper-v2"),
wrapper_decrypt_host=_host("APPLE_GATEWAY_WRAPPER_DECRYPT_HOST", "wrapper-v2"),
wrapper_decrypt_port=_positive_int("APPLE_GATEWAY_WRAPPER_DECRYPT_PORT", 10020, 1, 65535),
data_root=root,
cookies_path=Path(cookies).expanduser().resolve() if cookies else None,
storefront=storefront,
gamdl_path=os.getenv("APPLE_GATEWAY_GAMDL_PATH", "gamdl"),
ffmpeg_path=os.getenv("APPLE_GATEWAY_FFMPEG_PATH", "ffmpeg"),
subprocess_timeout_seconds=_positive_int("APPLE_GATEWAY_PROCESS_TIMEOUT_SECONDS", 900, 10, 7200),
wrapper_timeout_seconds=_positive_int("APPLE_GATEWAY_WRAPPER_TIMEOUT_SECONDS", 45, 2, 300),
max_concurrency=_positive_int("APPLE_GATEWAY_MAX_CONCURRENCY", 2, 1, 8),
max_process_output_bytes=_positive_int("APPLE_GATEWAY_MAX_PROCESS_OUTPUT_BYTES", 32768, 1024, 1048576),
)
def prepare(self) -> None:
self.data_root.mkdir(parents=True, exist_ok=True, mode=0o750)
for child in ("artifacts", "jobs", "temporary"):
(self.data_root / child).mkdir(exist_ok=True, mode=0o750)
@@ -0,0 +1,74 @@
from __future__ import annotations
import asyncio
import uuid
from dataclasses import dataclass, field
from pathlib import Path
from .models import DownloadJobView
from .runner import BoundedProcessRunner, ProcessFailure
from .security import safe_apple_url
@dataclass(slots=True)
class JobState:
id: str
media_kind: str
state: str = "queued"
artifacts: list[Path] = field(default_factory=list)
error_code: str | None = None
def view(self) -> DownloadJobView:
return DownloadJobView(
id=self.id,
state=self.state,
media_kind=self.media_kind,
artifact_count=len(self.artifacts),
error_code=self.error_code,
)
class DownloadJobManager:
def __init__(self, runner: BoundedProcessRunner, data_root: Path):
self._runner = runner
self._data_root = data_root
self._jobs: dict[str, JobState] = {}
self._tasks: set[asyncio.Task[None]] = set()
def enqueue(self, raw_url: str, quality: str) -> DownloadJobView:
url, media_kind = safe_apple_url(raw_url)
job_id = uuid.uuid4().hex
job = JobState(job_id, media_kind)
self._jobs[job_id] = job
task = asyncio.create_task(self._run(job, url, quality))
self._tasks.add(task)
task.add_done_callback(self._tasks.discard)
return job.view()
def get(self, job_id: str) -> DownloadJobView | None:
job = self._jobs.get(job_id)
return job.view() if job else None
async def close(self) -> None:
if not self._tasks:
return
for task in self._tasks:
task.cancel()
await asyncio.gather(*self._tasks, return_exceptions=True)
async def _run(self, job: JobState, url: str, quality: str) -> None:
job.state = "running"
root = self._data_root / "jobs" / job.id
try:
job.artifacts = await self._runner.download(url, quality, root / "artifacts", root / "temporary")
job.state = "succeeded"
except asyncio.CancelledError:
job.state = "failed"
job.error_code = "gateway_stopping"
raise
except ProcessFailure as exc:
job.state = "failed"
job.error_code = exc.code
except Exception:
job.state = "failed"
job.error_code = "download_failed"
@@ -0,0 +1,31 @@
from __future__ import annotations
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field
class StrictModel(BaseModel):
model_config = ConfigDict(extra="forbid")
class LoginRequest(StrictModel):
username: str = Field(min_length=1, max_length=320)
password: str = Field(min_length=1, max_length=1024)
class Login2faRequest(StrictModel):
code: str = Field(pattern=r"^[0-9]{4,8}$")
class DownloadJobRequest(StrictModel):
url: str = Field(min_length=1, max_length=2048)
quality: str = Field(default="alac", pattern=r"^(alac|aac|aac-web|aac-he|aac-he-web)$")
class DownloadJobView(StrictModel):
id: str
state: Literal["queued", "running", "succeeded", "failed"]
media_kind: str
artifact_count: int = 0
error_code: str | None = None
@@ -0,0 +1,99 @@
from __future__ import annotations
import asyncio
import os
import signal
from dataclasses import dataclass
from pathlib import Path
from .config import Settings
from .security import safe_files
@dataclass(frozen=True, slots=True)
class ProcessResult:
return_code: int
stdout: str
stderr: str
class ProcessFailure(RuntimeError):
def __init__(self, code: str):
super().__init__(code)
self.code = code
class BoundedProcessRunner:
def __init__(self, settings: Settings):
self._settings = settings
self._semaphore = asyncio.Semaphore(settings.max_concurrency)
async def execute(self, argv: list[str], cwd: Path) -> ProcessResult:
async with self._semaphore:
process = await asyncio.create_subprocess_exec(
*argv,
cwd=cwd,
stdin=asyncio.subprocess.DEVNULL,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
start_new_session=True,
)
stdout_task = asyncio.create_task(self._read_limited(process.stdout))
stderr_task = asyncio.create_task(self._read_limited(process.stderr))
try:
await asyncio.wait_for(process.wait(), timeout=self._settings.subprocess_timeout_seconds)
except TimeoutError as exc:
os.killpg(process.pid, signal.SIGKILL)
await process.wait()
await asyncio.gather(stdout_task, stderr_task)
raise ProcessFailure("process_timeout") from exc
stdout, stderr = await asyncio.gather(stdout_task, stderr_task)
return ProcessResult(process.returncode or 0, stdout, stderr)
async def _read_limited(self, stream: asyncio.StreamReader | None) -> str:
if stream is None:
return ""
retained = bytearray()
while chunk := await stream.read(8192):
remaining = self._settings.max_process_output_bytes - len(retained)
if remaining > 0:
retained.extend(chunk[:remaining])
return retained.decode("utf-8", errors="replace")
async def download(self, url: str, quality: str, output: Path, temporary: Path) -> list[Path]:
output.mkdir(parents=True, exist_ok=False, mode=0o750)
temporary.mkdir(parents=True, exist_ok=False, mode=0o750)
argv = [
self._settings.gamdl_path,
"--no-config-file",
"--no-exceptions",
"--use-wrapper",
"--wrapper-url", self._settings.wrapper_url,
"--wrapper-decrypt-host", self._settings.wrapper_decrypt_host,
"--wrapper-decrypt-port", str(self._settings.wrapper_decrypt_port),
"--output-path", str(output),
"--temp-path", str(temporary),
"--song-codec-priority", quality,
"--artist-auto-select", "all-albums",
]
if self._settings.cookies_path:
argv.extend(["--cookies-path", str(self._settings.cookies_path)])
argv.append(url)
result = await self.execute(argv, temporary)
if result.return_code != 0:
raise ProcessFailure("gamdl_failed")
artifacts = safe_files(output, {".m4a", ".flac", ".mp4", ".m4v", ".lrc", ".srt", ".ttml", ".jpg", ".png"})
if not artifacts:
raise ProcessFailure("artifact_missing")
return artifacts
async def to_flac(self, source: Path, target: Path) -> Path:
if source.suffix.lower() == ".flac":
return source
result = await self.execute([
self._settings.ffmpeg_path, "-nostdin", "-v", "error", "-y",
"-i", str(source), "-map", "0:a:0", "-c:a", "flac", str(target),
], target.parent)
if result.return_code != 0 or not target.is_file():
raise ProcessFailure("transcode_failed")
return target.resolve()
@@ -0,0 +1,59 @@
from __future__ import annotations
import re
from pathlib import Path
from typing import Any
from urllib.parse import urlsplit, urlunsplit
SECRET_KEY = re.compile(r"(authorization|cookie|password|secret|session|token)", re.IGNORECASE)
APPLE_ID = re.compile(r"^[0-9]{1,24}$")
SUPPORTED_APPLE_PATH = re.compile(
r"^/(?:[a-z]{2}/)?(?:album|song|playlist|artist|music-video|post|library)(?:/|$)",
re.IGNORECASE,
)
def sanitize_json(value: Any) -> Any:
if isinstance(value, dict):
return {key: sanitize_json(item) for key, item in value.items() if not SECRET_KEY.search(str(key))}
if isinstance(value, list):
return [sanitize_json(item) for item in value]
return value
def safe_apple_url(raw: str) -> tuple[str, str]:
parsed = urlsplit(raw.strip())
if (
parsed.scheme != "https"
or parsed.hostname != "music.apple.com"
or parsed.username
or parsed.password
or parsed.fragment
or not SUPPORTED_APPLE_PATH.match(parsed.path)
):
raise ValueError("unsupported_apple_music_url")
sanitized = urlunsplit(("https", "music.apple.com", parsed.path, parsed.query, ""))
segment = next((part for part in parsed.path.lower().split("/") if part in {
"album", "song", "playlist", "artist", "music-video", "post", "library"
}), "catalog")
return sanitized, segment
def song_url(storefront: str, song_id: str) -> str:
if not APPLE_ID.fullmatch(song_id):
raise ValueError("invalid_song_id")
return f"https://music.apple.com/{storefront}/song/{song_id}"
def safe_files(root: Path, suffixes: set[str]) -> list[Path]:
resolved_root = root.resolve()
result: list[Path] = []
for candidate in root.rglob("*"):
if candidate.is_symlink() or not candidate.is_file() or candidate.suffix.lower() not in suffixes:
continue
resolved = candidate.resolve()
if not resolved.is_relative_to(resolved_root):
continue
result.append(resolved)
return sorted(result, key=lambda item: item.as_posix())
@@ -0,0 +1,53 @@
from __future__ import annotations
from typing import Any
import httpx
from .security import sanitize_json
class WrapperResponse:
def __init__(self, status_code: int, payload: Any):
self.status_code = status_code
self.payload = payload
class WrapperClient:
def __init__(self, base_url: str, timeout_seconds: int, client: httpx.AsyncClient | None = None):
self._owns_client = client is None
self._client = client or httpx.AsyncClient(
base_url=f"{base_url}/",
timeout=httpx.Timeout(timeout_seconds),
follow_redirects=False,
trust_env=False,
)
async def close(self) -> None:
if self._owns_client:
await self._client.aclose()
async def request(self, method: str, path: str, payload: dict[str, str] | None = None) -> WrapperResponse:
try:
response = await self._client.request(method, path.lstrip("/"), json=payload)
except (httpx.TimeoutException, httpx.NetworkError):
return WrapperResponse(503, {"error": "wrapper_unavailable"})
if response.is_redirect:
return WrapperResponse(502, {"error": "wrapper_redirect_rejected"})
try:
body: Any = response.json()
except ValueError:
body = {"error": "invalid_wrapper_response"}
return WrapperResponse(response.status_code, sanitize_json(body))
async def health(self) -> WrapperResponse:
return await self.request("GET", "health")
async def me(self) -> WrapperResponse:
return await self.request("GET", "me")
async def login(self, username: str, password: str) -> WrapperResponse:
return await self.request("POST", "login", {"username": username, "password": password})
async def login_2fa(self, code: str) -> WrapperResponse:
return await self.request("POST", "login/2fa", {"code": code})
+28
View File
@@ -0,0 +1,28 @@
[build-system]
requires = ["setuptools==80.9.0"]
build-backend = "setuptools.build_meta"
[project]
name = "allstarr-apple-gateway"
version = "1.0.0"
requires-python = ">=3.10"
dependencies = [
"fastapi==0.116.1",
"gamdl==3.8.2",
"httpx==0.28.1",
"pydantic==2.11.7",
"uvicorn==0.35.0",
]
[project.optional-dependencies]
test = [
"pytest==8.4.1",
"pytest-asyncio==1.1.0",
]
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
[tool.setuptools.packages.find]
include = ["apple_gateway*"]
@@ -0,0 +1,254 @@
from __future__ import annotations
import asyncio
import sys
from dataclasses import replace
from pathlib import Path
from typing import Any
import httpx
import pytest
from fastapi.testclient import TestClient
from apple_gateway.app import API_VERSION, create_app
from apple_gateway.catalog import CatalogClient
from apple_gateway.config import Settings
from apple_gateway.runner import BoundedProcessRunner, ProcessFailure
from apple_gateway.wrapper import WrapperClient, WrapperResponse
class FakeWrapper:
def __init__(self, logged_in: bool = True):
self.logged_in = logged_in
self.login_payload: tuple[str, str] | None = None
async def close(self) -> None:
return None
async def health(self) -> WrapperResponse:
return WrapperResponse(200, {"status": "ok", "version": "0.0.2"})
async def me(self) -> WrapperResponse:
return WrapperResponse(200, {"auth": {"state": "authenticated" if self.logged_in else "logged_out"}})
async def login(self, username: str, password: str) -> WrapperResponse:
self.login_payload = (username, password)
return WrapperResponse(202, {"auth": {"state": "awaiting_2fa"}})
async def login_2fa(self, code: str) -> WrapperResponse:
return WrapperResponse(200, {"auth": {"state": "authenticated"}})
class FakeCatalog:
async def close(self) -> None:
return None
async def search_songs(self, query: str, limit: int) -> list[dict[str, Any]]:
return [song("101", query)][:limit]
async def song(self, song_id: str) -> dict[str, Any] | None:
return None if song_id == "404" else song(song_id, "Fixture")
class FakeRunner:
def __init__(self):
self.calls: list[tuple[str, str]] = []
async def download(self, url: str, quality: str, output: Path, temporary: Path) -> list[Path]:
self.calls.append((url, quality))
output.mkdir(parents=True, exist_ok=False)
temporary.mkdir(parents=True, exist_ok=False)
artifact = output / "fixture.m4a"
artifact.write_bytes(b"source")
return [artifact]
async def to_flac(self, source: Path, target: Path) -> Path:
target.write_bytes(b"fLaCfixture")
return target.resolve()
def song(song_id: str, title: str) -> dict[str, Any]:
return {
"id": song_id,
"title": title,
"artist": "Artist",
"album": "Album",
"duration": 123,
"cover_url": "https://example.test/art.jpg",
"track_number": 1,
"disc_number": 1,
"isrc": "USAAA0000001",
"release_date": "2026-01-01",
"copyright": None,
"composer": None,
"genre": "Pop",
}
@pytest.fixture
def settings(tmp_path: Path) -> Settings:
return Settings(
wrapper_url="http://wrapper-v2",
wrapper_decrypt_host="wrapper-v2",
wrapper_decrypt_port=18080,
data_root=tmp_path,
cookies_path=None,
storefront="us",
gamdl_path=sys.executable,
ffmpeg_path=sys.executable,
subprocess_timeout_seconds=2,
wrapper_timeout_seconds=2,
max_concurrency=1,
max_process_output_bytes=64,
)
@pytest.fixture
def client(settings: Settings) -> tuple[TestClient, FakeWrapper, FakeRunner]:
wrapper = FakeWrapper()
runner = FakeRunner()
app = create_app(settings, wrapper, FakeCatalog(), runner)
with TestClient(app) as test_client:
yield test_client, wrapper, runner
def test_capabilities_are_versioned_and_truthful(client):
response = client[0].get("/api/capabilities")
assert response.status_code == 200
assert response.json()["sidecarApiVersion"] == API_VERSION
ids = {item["id"] for item in response.json()["capabilities"]}
assert {"metadata-search-song", "metadata-song", "download-audio-song"} <= ids
def test_health_reports_wrapper_and_authentication(client):
payload = client[0].get("/api/health").json()
assert payload["wrapper_healthy"] is True
assert payload["logged_in"] is True
assert payload["versions"]["wrapper"] == "0.0.2"
def test_login_and_2fa_preserve_pending_status_without_returning_secrets(client):
response = client[0].post("/api/login", json={"username": "user@example.test", "password": "private"})
assert response.status_code == 202
assert "private" not in response.text
assert client[1].login_payload == ("user@example.test", "private")
assert client[0].post("/api/login/2fa", json={"code": "123456"}).status_code == 200
def test_search_song_and_missing_song_contract(client):
search = client[0].get("/api/search", params={"q": "Needle", "type": "song", "limit": 2})
assert search.status_code == 200
assert search.json()[0]["title"] == "Needle"
assert client[0].get("/api/song/101").json()["id"] == "101"
assert client[0].get("/api/song/404").status_code == 404
def test_song_download_uses_safe_id_quality_mapping_and_flac_contract(client):
response = client[0].get("/api/download/101", params={"quality": "alac-16-44"})
assert response.status_code == 200
assert response.headers["content-type"].startswith("audio/flac")
assert response.content == b"fLaCfixture"
assert client[2].calls == [("https://music.apple.com/us/song/101", "alac")]
assert client[0].get("/api/stream/102", params={"quality": "aac-320"}).status_code == 200
assert client[2].calls[-1] == ("https://music.apple.com/us/song/102", "aac")
assert client[0].get("/api/download/not-an-id").status_code == 400
@pytest.mark.asyncio
async def test_gamdl_command_targets_separate_wrapper_decrypt_socket(settings: Settings, tmp_path: Path):
class CapturingRunner(BoundedProcessRunner):
def __init__(self, configured: Settings):
super().__init__(configured)
self.argv: list[str] = []
async def execute(self, argv: list[str], cwd: Path):
from apple_gateway.runner import ProcessResult
self.argv = argv
(tmp_path / "output" / "fixture.m4a").parent.mkdir(parents=True, exist_ok=True)
(tmp_path / "output" / "fixture.m4a").write_bytes(b"fixture")
return ProcessResult(0, "", "")
runner = CapturingRunner(settings)
await runner.download(
"https://music.apple.com/us/song/101",
"alac",
tmp_path / "output",
tmp_path / "temporary",
)
host_index = runner.argv.index("--wrapper-decrypt-host")
port_index = runner.argv.index("--wrapper-decrypt-port")
assert runner.argv[host_index + 1] == "wrapper-v2"
assert runner.argv[port_index + 1] == "18080"
def test_generic_catalog_and_library_download_jobs_are_bounded_to_apple_urls(client):
accepted = client[0].post("/api/jobs/download", json={
"url": "https://music.apple.com/us/album/fixture/123?i=101",
"quality": "aac-web",
})
assert accepted.status_code == 202
job_id = accepted.json()["id"]
for _ in range(30):
state = client[0].get(f"/api/jobs/download/{job_id}").json()
if state["state"] == "succeeded":
break
asyncio.run(asyncio.sleep(0.01))
assert state["artifact_count"] == 1
library = client[0].post("/api/jobs/download", json={
"url": "https://music.apple.com/library/playlist/p.ABC123",
"quality": "alac",
})
assert library.status_code == 202
assert client[0].post("/api/jobs/download", json={
"url": "https://evil.example/album/123",
"quality": "alac",
}).status_code == 400
@pytest.mark.asyncio
async def test_process_runner_caps_output_and_times_out(settings: Settings, tmp_path: Path):
runner = BoundedProcessRunner(settings)
result = await runner.execute([sys.executable, "-c", "print('x' * 1000)"], tmp_path)
assert len(result.stdout.encode()) == settings.max_process_output_bytes
timeout_settings = replace(settings, subprocess_timeout_seconds=0.05)
timeout_runner = BoundedProcessRunner(timeout_settings)
with pytest.raises(ProcessFailure, match="process_timeout"):
await timeout_runner.execute([sys.executable, "-c", "import time; time.sleep(2)"], tmp_path)
@pytest.mark.asyncio
async def test_wrapper_client_rejects_redirects_and_redacts_nested_tokens():
async def handler(request: httpx.Request) -> httpx.Response:
if request.url.path == "/redirect":
return httpx.Response(302, headers={"location": "https://evil.example"})
return httpx.Response(200, json={"auth": {"state": "authenticated", "token": "secret"}})
http_client = httpx.AsyncClient(base_url="http://wrapper/", transport=httpx.MockTransport(handler), follow_redirects=False)
wrapper = WrapperClient("http://wrapper", 2, http_client)
assert (await wrapper.request("GET", "redirect")).payload["error"] == "wrapper_redirect_rejected"
payload = (await wrapper.me()).payload
assert payload == {"auth": {"state": "authenticated"}}
await http_client.aclose()
@pytest.mark.asyncio
async def test_catalog_mapping_is_deterministic():
async def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(200, json={"results": [{
"trackId": 101,
"trackName": "Fixture",
"artistName": "Artist",
"collectionName": "Album",
"trackTimeMillis": 123900,
"artworkUrl100": "https://img/100x100.jpg",
"releaseDate": "2026-01-02T00:00:00Z",
}]})
http_client = httpx.AsyncClient(base_url="https://itunes.apple.com/", transport=httpx.MockTransport(handler))
catalog = CatalogClient("us", http_client)
result = await catalog.search_songs("Fixture", 1)
assert result[0]["duration"] == 123
assert result[0]["cover_url"] == "https://img/1200x1200.jpg"
await http_client.aclose()
+986
View File
@@ -0,0 +1,986 @@
version = 1
revision = 3
requires-python = ">=3.10"
[[package]]
name = "allstarr-apple-gateway"
version = "1.0.0"
source = { editable = "." }
dependencies = [
{ name = "fastapi" },
{ name = "gamdl" },
{ name = "httpx" },
{ name = "pydantic" },
{ name = "uvicorn" },
]
[package.optional-dependencies]
test = [
{ name = "pytest" },
{ name = "pytest-asyncio" },
]
[package.metadata]
requires-dist = [
{ name = "fastapi", specifier = "==0.116.1" },
{ name = "gamdl", specifier = "==3.8.2" },
{ name = "httpx", specifier = "==0.28.1" },
{ name = "pydantic", specifier = "==2.11.7" },
{ name = "pytest", marker = "extra == 'test'", specifier = "==8.4.1" },
{ name = "pytest-asyncio", marker = "extra == 'test'", specifier = "==1.1.0" },
{ name = "uvicorn", specifier = "==0.35.0" },
]
provides-extras = ["test"]
[[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 = "anyio"
version = "4.14.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "idna" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" },
]
[[package]]
name = "async-lru"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e8/1f/989ecfef8e64109a489fff357450cb73fa73a865a92bd8c272170a6922c2/async_lru-2.3.0.tar.gz", hash = "sha256:89bdb258a0140d7313cf8f4031d816a042202faa61d0ab310a0a538baa1c24b6", size = 16332, upload-time = "2026-03-19T01:04:32.413Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/e2/c2e3abf398f80732e58b03be77bde9022550d221dd8781bf586bd4d97cc1/async_lru-2.3.0-py3-none-any.whl", hash = "sha256:eea27b01841909316f2cc739807acea1c623df2be8c5cfad7583286397bb8315", size = 8403, upload-time = "2026-03-19T01:04:30.883Z" },
]
[[package]]
name = "backports-asyncio-runner"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" },
]
[[package]]
name = "backports-datetime-fromisoformat"
version = "2.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/71/81/eff3184acb1d9dc3ce95a98b6f3c81a49b4be296e664db8e1c2eeabef3d9/backports_datetime_fromisoformat-2.0.3.tar.gz", hash = "sha256:b58edc8f517b66b397abc250ecc737969486703a66eb97e01e6d51291b1a139d", size = 23588, upload-time = "2024-12-28T20:18:15.017Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/42/4b/d6b051ca4b3d76f23c2c436a9669f3be616b8cf6461a7e8061c7c4269642/backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f681f638f10588fa3c101ee9ae2b63d3734713202ddfcfb6ec6cea0778a29d4", size = 27561, upload-time = "2024-12-28T20:16:47.974Z" },
{ url = "https://files.pythonhosted.org/packages/6d/40/e39b0d471e55eb1b5c7c81edab605c02f71c786d59fb875f0a6f23318747/backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:cd681460e9142f1249408e5aee6d178c6d89b49e06d44913c8fdfb6defda8d1c", size = 34448, upload-time = "2024-12-28T20:16:50.712Z" },
{ url = "https://files.pythonhosted.org/packages/f2/28/7a5c87c5561d14f1c9af979231fdf85d8f9fad7a95ff94e56d2205e2520a/backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:ee68bc8735ae5058695b76d3bb2aee1d137c052a11c8303f1e966aa23b72b65b", size = 27093, upload-time = "2024-12-28T20:16:52.994Z" },
{ url = "https://files.pythonhosted.org/packages/80/ba/f00296c5c4536967c7d1136107fdb91c48404fe769a4a6fd5ab045629af8/backports_datetime_fromisoformat-2.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8273fe7932db65d952a43e238318966eab9e49e8dd546550a41df12175cc2be4", size = 52836, upload-time = "2024-12-28T20:16:55.283Z" },
{ url = "https://files.pythonhosted.org/packages/e3/92/bb1da57a069ddd601aee352a87262c7ae93467e66721d5762f59df5021a6/backports_datetime_fromisoformat-2.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39d57ea50aa5a524bb239688adc1d1d824c31b6094ebd39aa164d6cadb85de22", size = 52798, upload-time = "2024-12-28T20:16:56.64Z" },
{ url = "https://files.pythonhosted.org/packages/df/ef/b6cfd355982e817ccdb8d8d109f720cab6e06f900784b034b30efa8fa832/backports_datetime_fromisoformat-2.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ac6272f87693e78209dc72e84cf9ab58052027733cd0721c55356d3c881791cf", size = 52891, upload-time = "2024-12-28T20:16:58.887Z" },
{ url = "https://files.pythonhosted.org/packages/37/39/b13e3ae8a7c5d88b68a6e9248ffe7066534b0cfe504bf521963e61b6282d/backports_datetime_fromisoformat-2.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:44c497a71f80cd2bcfc26faae8857cf8e79388e3d5fbf79d2354b8c360547d58", size = 52955, upload-time = "2024-12-28T20:17:00.028Z" },
{ url = "https://files.pythonhosted.org/packages/1e/e4/70cffa3ce1eb4f2ff0c0d6f5d56285aacead6bd3879b27a2ba57ab261172/backports_datetime_fromisoformat-2.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:6335a4c9e8af329cb1ded5ab41a666e1448116161905a94e054f205aa6d263bc", size = 29323, upload-time = "2024-12-28T20:17:01.125Z" },
{ url = "https://files.pythonhosted.org/packages/62/f5/5bc92030deadf34c365d908d4533709341fb05d0082db318774fdf1b2bcb/backports_datetime_fromisoformat-2.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2e4b66e017253cdbe5a1de49e0eecff3f66cd72bcb1229d7db6e6b1832c0443", size = 27626, upload-time = "2024-12-28T20:17:03.448Z" },
{ url = "https://files.pythonhosted.org/packages/28/45/5885737d51f81dfcd0911dd5c16b510b249d4c4cf6f4a991176e0358a42a/backports_datetime_fromisoformat-2.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:43e2d648e150777e13bbc2549cc960373e37bf65bd8a5d2e0cef40e16e5d8dd0", size = 34588, upload-time = "2024-12-28T20:17:04.459Z" },
{ url = "https://files.pythonhosted.org/packages/bc/6d/bd74de70953f5dd3e768c8fc774af942af0ce9f211e7c38dd478fa7ea910/backports_datetime_fromisoformat-2.0.3-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:4ce6326fd86d5bae37813c7bf1543bae9e4c215ec6f5afe4c518be2635e2e005", size = 27162, upload-time = "2024-12-28T20:17:06.752Z" },
{ url = "https://files.pythonhosted.org/packages/47/ba/1d14b097f13cce45b2b35db9898957578b7fcc984e79af3b35189e0d332f/backports_datetime_fromisoformat-2.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7c8fac333bf860208fd522a5394369ee3c790d0aa4311f515fcc4b6c5ef8d75", size = 54482, upload-time = "2024-12-28T20:17:08.15Z" },
{ url = "https://files.pythonhosted.org/packages/25/e9/a2a7927d053b6fa148b64b5e13ca741ca254c13edca99d8251e9a8a09cfe/backports_datetime_fromisoformat-2.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24a4da5ab3aa0cc293dc0662a0c6d1da1a011dc1edcbc3122a288cfed13a0b45", size = 54362, upload-time = "2024-12-28T20:17:10.605Z" },
{ url = "https://files.pythonhosted.org/packages/c1/99/394fb5e80131a7d58c49b89e78a61733a9994885804a0bb582416dd10c6f/backports_datetime_fromisoformat-2.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:58ea11e3bf912bd0a36b0519eae2c5b560b3cb972ea756e66b73fb9be460af01", size = 54162, upload-time = "2024-12-28T20:17:12.301Z" },
{ url = "https://files.pythonhosted.org/packages/88/25/1940369de573c752889646d70b3fe8645e77b9e17984e72a554b9b51ffc4/backports_datetime_fromisoformat-2.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8a375c7dbee4734318714a799b6c697223e4bbb57232af37fbfff88fb48a14c6", size = 54118, upload-time = "2024-12-28T20:17:13.609Z" },
{ url = "https://files.pythonhosted.org/packages/b7/46/f275bf6c61683414acaf42b2df7286d68cfef03e98b45c168323d7707778/backports_datetime_fromisoformat-2.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:ac677b1664c4585c2e014739f6678137c8336815406052349c85898206ec7061", size = 29329, upload-time = "2024-12-28T20:17:16.124Z" },
{ url = "https://files.pythonhosted.org/packages/a2/0f/69bbdde2e1e57c09b5f01788804c50e68b29890aada999f2b1a40519def9/backports_datetime_fromisoformat-2.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66ce47ee1ba91e146149cf40565c3d750ea1be94faf660ca733d8601e0848147", size = 27630, upload-time = "2024-12-28T20:17:19.442Z" },
{ url = "https://files.pythonhosted.org/packages/d5/1d/1c84a50c673c87518b1adfeafcfd149991ed1f7aedc45d6e5eac2f7d19d7/backports_datetime_fromisoformat-2.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:8b7e069910a66b3bba61df35b5f879e5253ff0821a70375b9daf06444d046fa4", size = 34707, upload-time = "2024-12-28T20:17:21.79Z" },
{ url = "https://files.pythonhosted.org/packages/71/44/27eae384e7e045cda83f70b551d04b4a0b294f9822d32dea1cbf1592de59/backports_datetime_fromisoformat-2.0.3-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:a3b5d1d04a9e0f7b15aa1e647c750631a873b298cdd1255687bb68779fe8eb35", size = 27280, upload-time = "2024-12-28T20:17:24.503Z" },
{ url = "https://files.pythonhosted.org/packages/a7/7a/a4075187eb6bbb1ff6beb7229db5f66d1070e6968abeb61e056fa51afa5e/backports_datetime_fromisoformat-2.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec1b95986430e789c076610aea704db20874f0781b8624f648ca9fb6ef67c6e1", size = 55094, upload-time = "2024-12-28T20:17:25.546Z" },
{ url = "https://files.pythonhosted.org/packages/71/03/3fced4230c10af14aacadc195fe58e2ced91d011217b450c2e16a09a98c8/backports_datetime_fromisoformat-2.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffe5f793db59e2f1d45ec35a1cf51404fdd69df9f6952a0c87c3060af4c00e32", size = 55605, upload-time = "2024-12-28T20:17:29.208Z" },
{ url = "https://files.pythonhosted.org/packages/f6/0a/4b34a838c57bd16d3e5861ab963845e73a1041034651f7459e9935289cfd/backports_datetime_fromisoformat-2.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:620e8e73bd2595dfff1b4d256a12b67fce90ece3de87b38e1dde46b910f46f4d", size = 55353, upload-time = "2024-12-28T20:17:32.433Z" },
{ url = "https://files.pythonhosted.org/packages/d9/68/07d13c6e98e1cad85606a876367ede2de46af859833a1da12c413c201d78/backports_datetime_fromisoformat-2.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4cf9c0a985d68476c1cabd6385c691201dda2337d7453fb4da9679ce9f23f4e7", size = 55298, upload-time = "2024-12-28T20:17:34.919Z" },
{ url = "https://files.pythonhosted.org/packages/60/33/45b4d5311f42360f9b900dea53ab2bb20a3d61d7f9b7c37ddfcb3962f86f/backports_datetime_fromisoformat-2.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:d144868a73002e6e2e6fef72333e7b0129cecdd121aa8f1edba7107fd067255d", size = 29375, upload-time = "2024-12-28T20:17:36.018Z" },
{ url = "https://files.pythonhosted.org/packages/be/03/7eaa9f9bf290395d57fd30d7f1f2f9dff60c06a31c237dc2beb477e8f899/backports_datetime_fromisoformat-2.0.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90e202e72a3d5aae673fcc8c9a4267d56b2f532beeb9173361293625fe4d2039", size = 28980, upload-time = "2024-12-28T20:18:06.554Z" },
{ url = "https://files.pythonhosted.org/packages/47/80/a0ecf33446c7349e79f54cc532933780341d20cff0ee12b5bfdcaa47067e/backports_datetime_fromisoformat-2.0.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2df98ef1b76f5a58bb493dda552259ba60c3a37557d848e039524203951c9f06", size = 28449, upload-time = "2024-12-28T20:18:07.77Z" },
]
[[package]]
name = "certifi"
version = "2026.6.17"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.9"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ad/81/8e983840c6e5b93b33c2ba81aa3d52c2e42f0e9a690ce7607a2e61da4a5c/charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a", size = 322240, upload-time = "2026-07-07T14:32:36.236Z" },
{ url = "https://files.pythonhosted.org/packages/de/d1/b4319dc3229d8272fba305e206fc0a148e2de8d4087917ce62ae6382f359/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616", size = 216475, upload-time = "2026-07-07T14:32:38.142Z" },
{ url = "https://files.pythonhosted.org/packages/80/33/6c99c1b3e6b8bf730e1bc809b9a2608f224145069114c479a2e9e1494346/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209", size = 238670, upload-time = "2026-07-07T14:32:39.658Z" },
{ url = "https://files.pythonhosted.org/packages/7f/f4/ffbb83546e1f198ecc70ecd372b65cf2b50f9068b380abd67640f17a8e18/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99", size = 233476, upload-time = "2026-07-07T14:32:41.155Z" },
{ url = "https://files.pythonhosted.org/packages/e8/5f/b98b8da398637b551e427e7be922bdec19177dc54d6811dcdaa503f23aac/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8", size = 223817, upload-time = "2026-07-07T14:32:42.592Z" },
{ url = "https://files.pythonhosted.org/packages/36/31/a276bb2e66243072a3fd06fdcab9cbb61a305b02143d70d2bda21d888fa8/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b", size = 207974, upload-time = "2026-07-07T14:32:44.258Z" },
{ url = "https://files.pythonhosted.org/packages/5e/be/7ee4453d7e88dfbc4104ccd34900b9f2c7c17dac22881865fe0e82424a25/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2", size = 221655, upload-time = "2026-07-07T14:32:45.64Z" },
{ url = "https://files.pythonhosted.org/packages/1d/85/181c652953eb5276d198f375b1dd641047392050098100a3a02d6534f657/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9", size = 219229, upload-time = "2026-07-07T14:32:47.376Z" },
{ url = "https://files.pythonhosted.org/packages/0c/e7/aaf6da33fc9f4691cda8f7efbc9f69179d3d39ec8a4799baf273ee1d8db0/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15", size = 209704, upload-time = "2026-07-07T14:32:48.855Z" },
{ url = "https://files.pythonhosted.org/packages/63/01/f2fb3bd3a73be48b173ee0c6aa8d2497af97d5663a8c4c4b491de4c62f7a/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d", size = 226243, upload-time = "2026-07-07T14:32:50.239Z" },
{ url = "https://files.pythonhosted.org/packages/c4/02/c57a22739fe05246b0b5783b3bfb6afaac4eebb46f3ececdfb2f048f780e/charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381", size = 150935, upload-time = "2026-07-07T14:32:51.676Z" },
{ url = "https://files.pythonhosted.org/packages/37/8d/ca39a7559a4797505530d084fd3a49a2c959efbbbff146302fb7be4e3b35/charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee", size = 162314, upload-time = "2026-07-07T14:32:53.193Z" },
{ url = "https://files.pythonhosted.org/packages/01/da/a44bd7a13d426e69e4894557106cd58669097bfad4a8681123b618fbfc5d/charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419", size = 153075, upload-time = "2026-07-07T14:32:54.554Z" },
{ url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" },
{ url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" },
{ url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" },
{ url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" },
{ url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" },
{ url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" },
{ url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" },
{ url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" },
{ url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" },
{ url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" },
{ url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" },
{ url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" },
{ url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" },
{ url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" },
{ url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" },
{ url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" },
{ url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" },
{ url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" },
{ url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" },
{ url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" },
{ url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" },
{ url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" },
{ url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" },
{ url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" },
{ url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" },
{ url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" },
{ url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" },
{ url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" },
{ url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" },
{ url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" },
{ url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" },
{ url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" },
{ url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" },
{ url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" },
{ url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" },
{ url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" },
{ url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" },
{ url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" },
{ url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" },
{ url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" },
{ url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" },
{ url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" },
{ url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" },
{ url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" },
{ url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" },
{ url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" },
{ url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" },
{ url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" },
{ url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" },
{ url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" },
{ url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" },
{ url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" },
{ url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" },
{ url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" },
{ url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" },
{ url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" },
{ url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" },
{ url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" },
{ url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" },
{ url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" },
{ url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" },
{ url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" },
{ url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" },
{ url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" },
{ url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" },
{ url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" },
]
[[package]]
name = "click"
version = "8.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" },
]
[[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 = "construct"
version = "2.8.8"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b6/2c/66bab4fef920ef8caa3e180ea601475b2cbbe196255b18f1c58215940607/construct-2.8.8.tar.gz", hash = "sha256:1b84b8147f6fd15bcf64b737c3e8ac5100811ad80c830cb4b2545140511c4157", size = 717694, upload-time = "2016-10-20T22:29:12.563Z" }
[[package]]
name = "dataclass-click"
version = "1.0.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
]
sdist = { url = "https://files.pythonhosted.org/packages/89/82/5b6035efd90621771fa039960eab3e1ec7ff2a8625033272856843e8bd27/dataclass_click-1.0.4.tar.gz", hash = "sha256:10e7de638dd9e68ae9abd5086f61d8ddee42b1873a70f5fd9fd2167856afac11", size = 7580, upload-time = "2025-10-10T21:11:31.956Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/86/dc/38a94a2eb5f756724a6dc87a7aea38f7b747fe7b2e9daabc34a65e6cd9ac/dataclass_click-1.0.4-py3-none-any.whl", hash = "sha256:a225d30c04e4abbdba411cc3d5ec0a2ea829e1dca6500afe5f87cc243e5ead72", size = 8553, upload-time = "2025-10-10T21:11:30.514Z" },
]
[[package]]
name = "exceptiongroup"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
]
[[package]]
name = "fastapi"
version = "0.116.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "starlette" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/78/d7/6c8b3bfe33eeffa208183ec037fee0cce9f7f024089ab1c5d12ef04bd27c/fastapi-0.116.1.tar.gz", hash = "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143", size = 296485, upload-time = "2025-07-11T16:22:32.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/47/d63c60f59a59467fda0f93f46335c9d18526d7071f025cb5b89d5353ea42/fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565", size = 95631, upload-time = "2025-07-11T16:22:30.485Z" },
]
[[package]]
name = "gamdl"
version = "3.8.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "async-lru" },
{ name = "click" },
{ name = "colorama" },
{ name = "dataclass-click" },
{ name = "httpx" },
{ name = "httpx-retries" },
{ name = "inquirerpy" },
{ name = "m3u8" },
{ name = "mutagen" },
{ name = "pillow" },
{ name = "pywidevine" },
{ name = "structlog" },
{ name = "yt-dlp" },
]
sdist = { url = "https://files.pythonhosted.org/packages/60/83/8d9979d75969a870cdbeae54bb9abfe0517de44810b666627e672b790d85/gamdl-3.8.2.tar.gz", hash = "sha256:6d70c292d9fee33bb36c62edcb559ac29a3a28413c257742fcfb9efc0d4d8b90", size = 76969, upload-time = "2026-07-09T15:37:24.83Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7a/3a/6c81ac32e98d4c71e0a4774a25efd47360211141f25de4bd9a2cd08eb3ce/gamdl-3.8.2-cp310-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c0f4c6d3d846ce9708c62a8841d63892ee8e6783ba002882926785e5420e70cd", size = 814872, upload-time = "2026-07-10T02:54:35.093Z" },
{ url = "https://files.pythonhosted.org/packages/78/7a/a3730cf0b91668fa1f8b5d16232c8864d114afd0de48012b5e25e6973a5a/gamdl-3.8.2-cp310-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3bf059a6dd2b3222db005d5a4cb23d5ca9bcb79e8031ac6563182dd034cc67bb", size = 487669, upload-time = "2026-07-10T03:03:45.823Z" },
{ url = "https://files.pythonhosted.org/packages/11/ef/ead06441f3b272d805ee54adab2574923a04792617d8ab1dfab68017a15d/gamdl-3.8.2-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:e22e3a23f490249d7fcc7fd951257ab11dbf6659ccc38517787b53c448c40946", size = 498644, upload-time = "2026-07-10T02:54:37.145Z" },
{ url = "https://files.pythonhosted.org/packages/95/ed/a727ff2983ceee794bfbd3c5ab1c0ebaffd57d31b0723b51fc4c69825186/gamdl-3.8.2-cp310-abi3-win_amd64.whl", hash = "sha256:173c1105b11887955e103c3cba3e525b159c6f6bb5337b558d2eefaeac0a3937", size = 368640, upload-time = "2026-07-10T02:54:38.913Z" },
{ url = "https://files.pythonhosted.org/packages/b3/64/c3988b2b4a75a15a47dfdcc1e9ba819aeb7d1834b401644fcbc28f7a89cc/gamdl-3.8.2-cp310-abi3-win_arm64.whl", hash = "sha256:acf6a2130786901dd1266966b5a2e69f38eb98d8c47d9c9add54157e31b2aa96", size = 357054, upload-time = "2026-07-10T03:03:47.173Z" },
{ url = "https://files.pythonhosted.org/packages/85/2a/10d19638599c25e24b22ae237e56666ebdc6a203a8ca2fd51cd3f3587a6b/gamdl-3.8.2-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:6da94f7429d7b72449c41e12d953a84037234dac9007977614ce48b596825772", size = 35279033, upload-time = "2026-07-09T15:33:41.09Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "httpcore"
version = "1.0.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
]
[[package]]
name = "httpx-retries"
version = "0.6.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e9/d3/b7a8bb09543af40009717a08a2ceba90b6d4c6f0cdf171404217d8f4c37d/httpx_retries-0.6.0.tar.gz", hash = "sha256:3e0b404969a564829d368417964fd21e6b400a10d17c92d29b8bc247ce8186e3", size = 21122, upload-time = "2026-07-06T00:52:30.093Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/56/c6/7f3d6ab3549267a1959161b38df4c0fb435eceaf2d531d8addfac01abaca/httpx_retries-0.6.0-py3-none-any.whl", hash = "sha256:d1e52a8f68a5df42de75ab89049d5020b2d0ab2f5f8bceacda008d12aa1257a3", size = 11776, upload-time = "2026-07-06T00:52:31.033Z" },
]
[[package]]
name = "idna"
version = "3.18"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
]
[[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 = "inquirerpy"
version = "0.3.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pfzy" },
{ name = "prompt-toolkit" },
]
sdist = { url = "https://files.pythonhosted.org/packages/64/73/7570847b9da026e07053da3bbe2ac7ea6cde6bb2cbd3c7a5a950fa0ae40b/InquirerPy-0.3.4.tar.gz", hash = "sha256:89d2ada0111f337483cb41ae31073108b2ec1e618a49d7110b0d7ade89fc197e", size = 44431, upload-time = "2022-06-27T23:11:20.598Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/ff/3b59672c47c6284e8005b42e84ceba13864aa0f39f067c973d1af02f5d91/InquirerPy-0.3.4-py3-none-any.whl", hash = "sha256:c65fdfbac1fa00e3ee4fb10679f4d3ed7a012abf4833910e63c295827fe2a7d4", size = 67677, upload-time = "2022-06-27T23:11:17.723Z" },
]
[[package]]
name = "m3u8"
version = "6.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "backports-datetime-fromisoformat", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9b/a5/73697aaa99bb32b610adc1f11d46a0c0c370351292e9b271755084a145e6/m3u8-6.0.0.tar.gz", hash = "sha256:7ade990a1667d7a653bcaf9413b16c3eb5cd618982ff46aaff57fe6d9fa9c0fd", size = 42720, upload-time = "2024-08-07T11:20:06.606Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/31/50f3c38b38ff28635ff9c4a4afefddccc5f1b57457b539bdbdf75ce18669/m3u8-6.0.0-py3-none-any.whl", hash = "sha256:566d0748739c552dad10f8c87150078de6a0ec25071fa48e6968e96fc6dcba5d", size = 24133, upload-time = "2024-08-07T11:20:03.96Z" },
]
[[package]]
name = "mutagen"
version = "1.48.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/df/70/1675da133ea92227da41bf5b24e1c66be597ff736a1533ade41da986852f/mutagen-1.48.1.tar.gz", hash = "sha256:8f95637ab9f6f305cec6bd1294e197debe207998e3e068596563c74f86b0a173", size = 1276978, upload-time = "2026-06-25T09:47:32.443Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/47/d8/a29e4e3991765e7ce4ed1f7e4074fe1ba9da03e0048639734de60f9cadb9/mutagen-1.48.1-py3-none-any.whl", hash = "sha256:4f077fe87d3fc7fba259aa63d8c026b18382ca6a42ef37c61e16f1b1b5b82fe7", size = 195706, upload-time = "2026-06-25T09:47:30.296Z" },
]
[[package]]
name = "packaging"
version = "26.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
]
[[package]]
name = "pfzy"
version = "0.3.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d9/5a/32b50c077c86bfccc7bed4881c5a2b823518f5450a30e639db5d3711952e/pfzy-0.3.4.tar.gz", hash = "sha256:717ea765dd10b63618e7298b2d98efd819e0b30cd5905c9707223dceeb94b3f1", size = 8396, upload-time = "2022-01-28T02:26:17.946Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8c/d7/8ff98376b1acc4503253b685ea09981697385ce344d4e3935c2af49e044d/pfzy-0.3.4-py3-none-any.whl", hash = "sha256:5f50d5b2b3207fa72e7ec0ef08372ef652685470974a107d0d4999fc5a903a96", size = 8537, upload-time = "2022-01-28T02:26:16.047Z" },
]
[[package]]
name = "pillow"
version = "12.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/25/c2/669d88644cddb1485bd9534e63e8cf476c8e51cb3c3a1297677023505c0e/pillow-12.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a", size = 5392418, upload-time = "2026-07-01T11:53:27.808Z" },
{ url = "https://files.pythonhosted.org/packages/6b/ba/3762f376a2948e3036488d773a146e0ae6ecc2ca03ac20e2615bd0b2ba02/pillow-12.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7", size = 4785287, upload-time = "2026-07-01T11:53:29.761Z" },
{ url = "https://files.pythonhosted.org/packages/07/50/b5d688cc9c52d4482f3d5bcab6ce20bc2a74a85d2343841c907444a3be2c/pillow-12.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f", size = 6253754, upload-time = "2026-07-01T11:53:32.298Z" },
{ url = "https://files.pythonhosted.org/packages/4e/89/36f4cd76cf4baf05c50ababb976249153f18c959171c7f6ba09a6f217260/pillow-12.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec", size = 6925605, upload-time = "2026-07-01T11:53:34.487Z" },
{ url = "https://files.pythonhosted.org/packages/eb/c0/4de58cf6633b9e3a6061ef4be6fb91fc3c90b812ece886f531e3c523d777/pillow-12.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468", size = 6327788, upload-time = "2026-07-01T11:53:36.433Z" },
{ url = "https://files.pythonhosted.org/packages/87/3c/14d53682a19550dbbaf3b598f807d5457646c510805a44c7d7891cd1cd1a/pillow-12.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed", size = 7036288, upload-time = "2026-07-01T11:53:38.712Z" },
{ url = "https://files.pythonhosted.org/packages/38/1d/36279e3c77efe034e4cc2b0393ee74ffdb5a62391dacbf9b916154f5f0b8/pillow-12.3.0-cp310-cp310-win32.whl", hash = "sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1", size = 6472396, upload-time = "2026-07-01T11:53:40.781Z" },
{ url = "https://files.pythonhosted.org/packages/48/7c/8fa0039574c476d7c6fa57dd7c32a130436877c6ec1e5ce1cc8ec44878c1/pillow-12.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb", size = 7226887, upload-time = "2026-07-01T11:53:42.764Z" },
{ url = "https://files.pythonhosted.org/packages/fa/17/e324be141d173c1c919428066c3259f21c1b8982e564e01a4a81e96dbdcf/pillow-12.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f", size = 2568039, upload-time = "2026-07-01T11:53:45.372Z" },
{ url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" },
{ url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" },
{ url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" },
{ url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" },
{ url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" },
{ url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" },
{ url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" },
{ url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" },
{ url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" },
{ url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" },
{ url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" },
{ url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" },
{ url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" },
{ url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" },
{ url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" },
{ url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" },
{ url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" },
{ url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" },
{ url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" },
{ url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" },
{ url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" },
{ url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" },
{ url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" },
{ url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" },
{ url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" },
{ url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" },
{ url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" },
{ url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" },
{ url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" },
{ url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" },
{ url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" },
{ url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" },
{ url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" },
{ url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" },
{ url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" },
{ url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" },
{ url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" },
{ url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" },
{ url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" },
{ url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" },
{ url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" },
{ url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" },
{ url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" },
{ url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" },
{ url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" },
{ url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" },
{ url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" },
{ url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" },
{ url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" },
{ url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" },
{ url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" },
{ url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" },
{ url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" },
{ url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" },
{ url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" },
{ url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" },
{ url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" },
{ url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" },
{ url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" },
{ url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" },
{ url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" },
{ url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" },
{ url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" },
{ url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" },
{ url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" },
{ url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" },
{ url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" },
{ url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" },
{ url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" },
{ url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" },
{ url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" },
{ url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" },
{ url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" },
{ url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" },
{ url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" },
{ url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" },
{ url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" },
]
[[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 = "prompt-toolkit"
version = "3.0.52"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "wcwidth" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
]
[[package]]
name = "protobuf"
version = "6.33.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" },
{ url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" },
{ url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" },
{ url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" },
{ url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" },
{ url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" },
{ url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" },
]
[[package]]
name = "pycryptodome"
version = "3.23.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" },
{ url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" },
{ url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" },
{ url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" },
{ url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" },
{ url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" },
{ url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" },
{ url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" },
{ url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" },
{ url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" },
{ url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" },
{ url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" },
{ url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" },
{ url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" },
{ url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" },
{ url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" },
{ url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" },
{ url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" },
{ url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" },
{ url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" },
{ url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" },
{ url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" },
{ url = "https://files.pythonhosted.org/packages/d9/12/e33935a0709c07de084d7d58d330ec3f4daf7910a18e77937affdb728452/pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379", size = 1623886, upload-time = "2025-05-17T17:21:20.614Z" },
{ url = "https://files.pythonhosted.org/packages/22/0b/aa8f9419f25870889bebf0b26b223c6986652bdf071f000623df11212c90/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4", size = 1672151, upload-time = "2025-05-17T17:21:22.666Z" },
{ url = "https://files.pythonhosted.org/packages/d4/5e/63f5cbde2342b7f70a39e591dbe75d9809d6338ce0b07c10406f1a140cdc/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630", size = 1664461, upload-time = "2025-05-17T17:21:25.225Z" },
{ url = "https://files.pythonhosted.org/packages/d6/92/608fbdad566ebe499297a86aae5f2a5263818ceeecd16733006f1600403c/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353", size = 1702440, upload-time = "2025-05-17T17:21:27.991Z" },
{ url = "https://files.pythonhosted.org/packages/d1/92/2eadd1341abd2989cce2e2740b4423608ee2014acb8110438244ee97d7ff/pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5", size = 1803005, upload-time = "2025-05-17T17:21:31.37Z" },
]
[[package]]
name = "pydantic"
version = "2.11.7"
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/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" },
]
[[package]]
name = "pydantic-core"
version = "2.33.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817, upload-time = "2025-04-23T18:30:43.919Z" },
{ url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357, upload-time = "2025-04-23T18:30:46.372Z" },
{ url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011, upload-time = "2025-04-23T18:30:47.591Z" },
{ url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730, upload-time = "2025-04-23T18:30:49.328Z" },
{ url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178, upload-time = "2025-04-23T18:30:50.907Z" },
{ url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462, upload-time = "2025-04-23T18:30:52.083Z" },
{ url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652, upload-time = "2025-04-23T18:30:53.389Z" },
{ url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306, upload-time = "2025-04-23T18:30:54.661Z" },
{ url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720, upload-time = "2025-04-23T18:30:56.11Z" },
{ url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915, upload-time = "2025-04-23T18:30:57.501Z" },
{ url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884, upload-time = "2025-04-23T18:30:58.867Z" },
{ url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496, upload-time = "2025-04-23T18:31:00.078Z" },
{ url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019, upload-time = "2025-04-23T18:31:01.335Z" },
{ url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" },
{ url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" },
{ url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" },
{ url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" },
{ url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" },
{ url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" },
{ url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" },
{ url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" },
{ url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" },
{ url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" },
{ url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" },
{ url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" },
{ url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" },
{ url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" },
{ url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" },
{ url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" },
{ url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" },
{ url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" },
{ url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" },
{ url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" },
{ url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" },
{ url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" },
{ url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" },
{ url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" },
{ url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" },
{ url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" },
{ url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" },
{ url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" },
{ url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" },
{ url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" },
{ url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" },
{ url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" },
{ url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" },
{ url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" },
{ url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" },
{ url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" },
{ url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" },
{ url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" },
{ url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" },
{ url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" },
{ url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" },
{ url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" },
{ url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" },
{ url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" },
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
{ url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982, upload-time = "2025-04-23T18:32:53.14Z" },
{ url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412, upload-time = "2025-04-23T18:32:55.52Z" },
{ url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749, upload-time = "2025-04-23T18:32:57.546Z" },
{ url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527, upload-time = "2025-04-23T18:32:59.771Z" },
{ url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225, upload-time = "2025-04-23T18:33:04.51Z" },
{ url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490, upload-time = "2025-04-23T18:33:06.391Z" },
{ url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525, upload-time = "2025-04-23T18:33:08.44Z" },
{ url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446, upload-time = "2025-04-23T18:33:10.313Z" },
{ url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678, upload-time = "2025-04-23T18:33:12.224Z" },
{ url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" },
{ url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" },
{ url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" },
{ url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" },
{ url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" },
{ url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" },
{ url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" },
{ url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" },
{ url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" },
]
[[package]]
name = "pygments"
version = "2.20.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
]
[[package]]
name = "pymp4"
version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "construct" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a5/46/dfb3f5363fc71adaf419147fdcb93341029ca638634a5cc6f7e7446416b2/pymp4-1.4.0.tar.gz", hash = "sha256:bc9e77732a8a143d34c38aa862a54180716246938e4bf3e07585d19252b77bb5", size = 13018, upload-time = "2023-05-07T15:01:34.02Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/aa/a2/27fea39af627c0ce5dbf6108bf969ea8f5fc9376d29f11282a80e3426f1d/pymp4-1.4.0-py3-none-any.whl", hash = "sha256:3401666c1e2a97ac94dffb18c5a5dcbd46d0a436da5272d378a6f9f6506dd12d", size = 14832, upload-time = "2023-05-07T15:01:32.293Z" },
]
[[package]]
name = "pytest"
version = "8.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" },
]
[[package]]
name = "pytest-asyncio"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" },
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4e/51/f8794af39eeb870e87a8c8068642fc07bce0c854d6865d7dd0f2a9d338c2/pytest_asyncio-1.1.0.tar.gz", hash = "sha256:796aa822981e01b68c12e4827b8697108f7205020f24b5793b3c41555dab68ea", size = 46652, upload-time = "2025-07-16T04:29:26.393Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/9d/bf86eddabf8c6c9cb1ea9a869d6873b46f105a5d292d3a6f7071f5b07935/pytest_asyncio-1.1.0-py3-none-any.whl", hash = "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf", size = 15157, upload-time = "2025-07-16T04:29:24.929Z" },
]
[[package]]
name = "pywidevine"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "protobuf" },
{ name = "pycryptodome" },
{ name = "pymp4" },
{ name = "pyyaml" },
{ name = "requests" },
{ name = "unidecode" },
]
sdist = { url = "https://files.pythonhosted.org/packages/52/b6/4855cb958892653029f3cafa8a4724d554b847de0a43a3808cea109b9e78/pywidevine-1.9.0.tar.gz", hash = "sha256:6742daf5fd797c5a4813eb1300efb3181ffcddd0c8c478ee28c7c536aa0e51b2", size = 75511, upload-time = "2025-10-27T09:13:15.909Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dd/e2/e692647f12008495654868e99c972696f2b8cb39637b507c409f5f99b4b6/pywidevine-1.9.0-py3-none-any.whl", hash = "sha256:70b5726abc2c3fe763f070da853b7c87d6aeb6131a27778743187258bf97e492", size = 70419, upload-time = "2025-10-27T09:13:14.303Z" },
]
[[package]]
name = "pyyaml"
version = "6.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" },
{ url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" },
{ url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" },
{ url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" },
{ url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" },
{ url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" },
{ url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" },
{ url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" },
{ url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" },
{ url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
{ url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
{ url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
{ url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
{ url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
{ url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
{ url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
{ url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
{ url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
]
[[package]]
name = "requests"
version = "2.34.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
]
[[package]]
name = "starlette"
version = "0.47.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/15/b9/cc3017f9a9c9b6e27c5106cc10cc7904653c3eec0729793aec10479dd669/starlette-0.47.3.tar.gz", hash = "sha256:6bc94f839cc176c4858894f1f8908f0ab79dfec1a6b8402f6da9be26ebea52e9", size = 2584144, upload-time = "2025-08-24T13:36:42.122Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/fd/901cfa59aaa5b30a99e16876f11abe38b59a1a2c51ffb3d7142bb6089069/starlette-0.47.3-py3-none-any.whl", hash = "sha256:89c0778ca62a76b826101e7c709e70680a1699ca7da6b44d38eb0a7e61fe4b51", size = 72991, upload-time = "2025-08-24T13:36:40.887Z" },
]
[[package]]
name = "structlog"
version = "26.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5e/89/b4a0bcfdf4f71a3dea31379f095929613d7e4528a0996bca6aa964cd0dca/structlog-26.1.0.tar.gz", hash = "sha256:f63a716cbd1b1291cf7661de7794b455acfa4c43c5bcf1630e6ad5ddc1adb3b7", size = 1459881, upload-time = "2026-06-06T07:33:39.348Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a9/18/489c97b834dfff9cf2fc2507cede4bcd4b11e67f84bc462acd1992496f86/structlog-26.1.0-py3-none-any.whl", hash = "sha256:e081a26d6c373e6d201eca24eede26d8ffab07f88f477822e679183428d3d91e", size = 73764, upload-time = "2026-06-06T07:33:38.046Z" },
]
[[package]]
name = "tomli"
version = "2.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" },
{ url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" },
{ url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" },
{ url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" },
{ url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" },
{ url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" },
{ url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" },
{ url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" },
{ url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" },
{ url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
{ url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
{ url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
{ url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
{ url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
{ url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
{ url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
{ url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
{ url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
{ url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
{ url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
{ url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
{ url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
{ url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
{ url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
{ url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
{ url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
{ url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
{ url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
{ url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
{ url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
{ url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
{ url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
{ url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
{ url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
{ url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
{ url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
{ url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
{ url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
{ url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
{ url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
{ url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
{ url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
{ url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
{ url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
{ url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
{ url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
]
[[package]]
name = "typing-extensions"
version = "4.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
]
[[package]]
name = "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 = "unidecode"
version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/7d/a8a765761bbc0c836e397a2e48d498305a865b70a8600fd7a942e85dcf63/Unidecode-1.4.0.tar.gz", hash = "sha256:ce35985008338b676573023acc382d62c264f307c8f7963733405add37ea2b23", size = 200149, upload-time = "2025-04-24T08:45:03.798Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8f/b7/559f59d57d18b44c6d1250d2eeaa676e028b9c527431f5d0736478a73ba1/Unidecode-1.4.0-py3-none-any.whl", hash = "sha256:c3c7606c27503ad8d501270406e345ddb480a7b5f38827eafe4fa82a137f0021", size = 235837, upload-time = "2025-04-24T08:45:01.609Z" },
]
[[package]]
name = "urllib3"
version = "2.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
]
[[package]]
name = "uvicorn"
version = "0.35.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" },
]
[[package]]
name = "wcwidth"
version = "0.8.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" },
]
[[package]]
name = "yt-dlp"
version = "2026.7.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/47/c5/9972af4b472b0d55badf841ebafd2f98944cb0ae0f46e11d01f363ea5b91/yt_dlp-2026.7.4.tar.gz", hash = "sha256:b094813404f87a9dd2186f00815231df32e5fd8a5403be0f807b3bb2d21a4432", size = 3049326, upload-time = "2026-07-04T22:42:14.837Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/8a/cd4c9b02c10c563adfe78118310129641900e1cd6de888cfae2452072696/yt_dlp-2026.7.4-py3-none-any.whl", hash = "sha256:f11f2b11d5a8ac4059f9bdf29fa4407dc7c6bb00c5097e95ca22a7a9db518266", size = 3184705, upload-time = "2026-07-04T22:42:12.989Z" },
]
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
LOCK="$ROOT/tools/apple-provider/source-lock.json"
OUTPUT="$ROOT/.apple-provider/wrapper-v2"
ARCH=x86_64
PACKAGE=""
STAGED_LIBS=""
usage() {
echo "Usage: $0 [--apk FILE | --apkm FILE | --staged-libs DIR] [--arch x86_64|arm64-v8a] [--output DIR]" >&2
}
while (($#)); do
case "$1" in
--apk|--apkm) PACKAGE=${2:?missing package path}; shift 2 ;;
--staged-libs) STAGED_LIBS=${2:?missing staged library directory}; shift 2 ;;
--arch) ARCH=${2:?missing architecture}; shift 2 ;;
--output) OUTPUT=${2:?missing output directory}; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) usage; exit 2 ;;
esac
done
[[ "$ARCH" == x86_64 || "$ARCH" == arm64-v8a ]] || { echo "Unsupported architecture: $ARCH" >&2; exit 2; }
[[ -n "$PACKAGE" || -n "$STAGED_LIBS" ]] || { usage; exit 2; }
[[ ! ( -n "$PACKAGE" && -n "$STAGED_LIBS" ) ]] || { echo "Choose a package or --staged-libs, not both." >&2; exit 2; }
for command in git jq unzip; do command -v "$command" >/dev/null || { echo "Missing required command: $command" >&2; exit 1; }; done
repository=$(jq -r '.wrapper.repository' "$LOCK")
tag=$(jq -r '.wrapper.tag' "$LOCK")
commit=$(jq -r '.wrapper.commit' "$LOCK")
if [[ -e "$OUTPUT" ]]; then
[[ -d "$OUTPUT/.git" ]] || { echo "Output exists and is not a wrapper-v2 checkout: $OUTPUT" >&2; exit 1; }
else
mkdir -p "$(dirname "$OUTPUT")"
git clone --filter=blob:none --branch "$tag" --single-branch "$repository" "$OUTPUT"
fi
git -C "$OUTPUT" fetch --force --tags origin "refs/tags/$tag:refs/tags/$tag"
tag_commit=$(git -C "$OUTPUT" rev-list -n 1 "$tag")
[[ "$tag_commit" == "$commit" ]] || { echo "wrapper-v2 tag $tag resolved to unexpected commit $tag_commit" >&2; exit 1; }
git -C "$OUTPUT" checkout --detach "$commit"
[[ -f "$OUTPUT/LIBS_VERSION.json" ]] || { echo "Pinned wrapper source lacks LIBS_VERSION.json" >&2; exit 1; }
if [[ -n "$PACKAGE" ]]; then
[[ -f "$PACKAGE" ]] || { echo "Package not found: $PACKAGE" >&2; exit 1; }
bash "$OUTPUT/tools/extract-libs.sh" --bundle "$PACKAGE" --arch "$ARCH"
else
[[ -d "$STAGED_LIBS" ]] || { echo "Staged library directory not found: $STAGED_LIBS" >&2; exit 1; }
mkdir -p "$OUTPUT/rootfs/system/lib64"
while IFS= read -r library; do
source="$STAGED_LIBS/$library"
[[ -f "$source" ]] || { echo "Missing pinned staged library: $library" >&2; exit 1; }
install -m 0644 "$source" "$OUTPUT/rootfs/system/lib64/$library"
done < <(jq -r --arg arch "$ARCH" '.libs[$arch] | keys[]' "$OUTPUT/LIBS_VERSION.json")
fi
bash "$OUTPUT/tools/stage-system.sh" --arch "$ARCH"
sha256_file() { command -v sha256sum >/dev/null && sha256sum "$1" | awk '{print $1}' || shasum -a 256 "$1" | awk '{print $1}'; }
verify_group() {
local group=$1 base=$2
while IFS=$'\t' read -r relative expected; do
file="$base/$relative"
[[ -f "$file" ]] || { echo "Missing pinned file: $relative" >&2; exit 1; }
actual=$(sha256_file "$file")
[[ "$actual" == "$expected" ]] || { echo "Hash mismatch for $relative" >&2; exit 1; }
done < <(jq -r --arg arch "$ARCH" --arg group "${group#.}" '.[$group][$arch] | to_entries[] | [.key,.value] | @tsv' "$OUTPUT/LIBS_VERSION.json")
}
verify_group android_system "$OUTPUT/rootfs/system"
verify_group libs "$OUTPUT/rootfs/system/lib64"
printf 'Prepared verified wrapper-v2 %s (%s) at %s\n' "$tag" "$tag_commit" "$OUTPUT"
printf 'Next: run ./allstarr.sh up, then finish Apple login in the WebUI.\n'
+12
View File
@@ -0,0 +1,12 @@
{
"wrapper": {
"repository": "https://github.com/glomatico/wrapper-v2.git",
"tag": "0.0.2",
"commit": "0bc16acb55f557b5c98d49f21d7af685410f7f8b"
},
"appleMusic": {
"package": "com.apple.android.music",
"version": "3.6.0-beta",
"build": "1109"
}
}