mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-10-08 06:05:02 -04:00
feat(apple): add turnkey optional download provider
This commit is contained in:
40 files changed
+3196
-96
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
]);
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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:
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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"]
|
||||
@@ -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."""
|
||||
@@ -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})
|
||||
@@ -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()
|
||||
Generated
+986
@@ -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" },
|
||||
]
|
||||
Executable
+76
@@ -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'
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user