diff --git a/docs/detectors/uv.md b/docs/detectors/uv.md new file mode 100644 index 000000000..e9835f4b7 --- /dev/null +++ b/docs/detectors/uv.md @@ -0,0 +1,6 @@ +# uv Detection +## Requirements +[uv](https://docs.astral.sh/uv/) detection relies on a [uv.lock](https://docs.astral.sh/uv/concepts/projects/layout/#the-lockfile) file being present. + +## Detection strategy +uv detection is performed by parsing a uv.lock found under the scan directory. diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvDependency.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvDependency.cs new file mode 100644 index 000000000..41a87fb5c --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvDependency.cs @@ -0,0 +1,10 @@ +#nullable enable +namespace Microsoft.ComponentDetection.Detectors.Uv +{ + public class UvDependency + { + public required string Name { get; init; } + + public string? Specifier { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs new file mode 100644 index 000000000..c74fa149b --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs @@ -0,0 +1,144 @@ +#nullable enable +namespace Microsoft.ComponentDetection.Detectors.Uv +{ + using System; + using System.Collections.Generic; + using System.IO; + using Tomlyn; + using Tomlyn.Model; + + public class UvLock + { + // a list of packages with their dependencies + public List Packages { get; set; } = []; + + // static method to parse the TOML stream into a UvLock model + public static UvLock Parse(Stream tomlStream) + { + using var reader = new StreamReader(tomlStream); + var tomlContent = reader.ReadToEnd(); + var model = Toml.ToModel(tomlContent); + return new UvLock + { + Packages = ParsePackagesFromModel(model), + }; + } + + internal static List ParsePackagesFromModel(object? model) + { + if (model is not TomlTable table) + { + throw new InvalidOperationException("TOML root is not a table"); + } + + if (!table.TryGetValue("package", out var packagesObj) || packagesObj is not TomlTableArray packages) + { + return []; + } + + var result = new List(); + foreach (var pkg in packages) + { + var parsed = ParsePackage(pkg); + if (parsed is not null) + { + result.Add(parsed); + } + } + + return result; + } + + internal static UvPackage? ParsePackage(object? pkg) + { + if (pkg is not TomlTable pkgTable) + { + return null; + } + + if (pkgTable.TryGetValue("name", out var nameObj) && nameObj is string name && + pkgTable.TryGetValue("version", out var versionObj) && versionObj is string version) + { + var uvPackage = new UvPackage + { + Name = name, + Version = version, + Dependencies = [], + MetadataRequiresDist = [], + MetadataRequiresDev = [], + }; + + if (pkgTable.TryGetValue("dependencies", out var depsObj) && depsObj is TomlArray depsArray) + { + uvPackage.Dependencies = ParseDependenciesArray(depsArray); + } + + if (pkgTable.TryGetValue("metadata", out var metadataObj) && metadataObj is TomlTable metadataTable) + { + ParseMetadata(metadataTable, uvPackage); + } + + // Parse source + if (pkgTable.TryGetValue("source", out var sourceObj) && sourceObj is TomlTable sourceTable) + { + var source = new UvSource + { + Registry = sourceTable.TryGetValue("registry", out var regObj) && regObj is string reg ? reg : null, + Virtual = sourceTable.TryGetValue("virtual", out var virtObj) && virtObj is string virt ? virt : null, + }; + uvPackage.Source = source; + } + + return uvPackage; + } + + return null; + } + + internal static List ParseDependenciesArray(TomlArray? depsArray) + { + var deps = new List(); + if (depsArray is null) + { + return deps; + } + + foreach (var dep in depsArray) + { + if (dep is TomlTable depTable && + depTable.TryGetValue("name", out var depNameObj) && depNameObj is string depName) + { + var depSpec = depTable.TryGetValue("specifier", out var specObj) && specObj is string s ? s : null; + deps.Add(new UvDependency + { + Name = depName, + Specifier = depSpec, + }); + } + } + + return deps; + } + + internal static void ParseMetadata(TomlTable? metadataTable, UvPackage uvPackage) + { + if (metadataTable is null) + { + return; + } + + if (metadataTable.TryGetValue("requires-dist", out var requiresDistObj) && requiresDistObj is TomlArray requiresDistArr) + { + uvPackage.MetadataRequiresDist = ParseDependenciesArray(requiresDistArr); + } + + if (metadataTable.TryGetValue("requires-dev", out var requiresDevObj) && requiresDevObj is TomlTable requiresDevTable) + { + if (requiresDevTable.TryGetValue("dev", out var devObj) && devObj is TomlArray devArr) + { + uvPackage.MetadataRequiresDev = ParseDependenciesArray(devArr); + } + } + } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs new file mode 100644 index 000000000..d111e8487 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs @@ -0,0 +1,106 @@ +#nullable enable + +namespace Microsoft.ComponentDetection.Detectors.Uv +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.ComponentDetection.Contracts; + using Microsoft.ComponentDetection.Contracts.Internal; + using Microsoft.ComponentDetection.Contracts.TypedComponent; + using Microsoft.Extensions.Logging; + + public class UvLockComponentDetector : FileComponentDetector, IDefaultOffComponentDetector + { + public UvLockComponentDetector( + IComponentStreamEnumerableFactory componentStreamEnumerableFactory, + IObservableDirectoryWalkerFactory walkerFactory, + ILogger logger) + { + this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory; + this.Scanner = walkerFactory; + this.Logger = logger; + } + + public override string Id => "UvLock"; + + public override IList SearchPatterns { get; } = ["uv.lock"]; + + public override IEnumerable SupportedComponentTypes => [ComponentType.Pip]; + + public override int Version => 1; + + public override IEnumerable Categories => ["Python"]; + + internal static bool IsRootPackage(UvPackage pck) + { + return pck.Source?.Virtual != null; + } + + protected override Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary detectorArgs, CancellationToken cancellationToken = default) + { + var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder; + var file = processRequest.ComponentStream; + + try + { + // Parse the file stream into a UvLock model + file.Stream.Position = 0; // Ensure stream is at the beginning + var uvLock = UvLock.Parse(file.Stream); + + var rootPackage = uvLock.Packages.FirstOrDefault(IsRootPackage); + var explicitPackages = new HashSet(); + var devPackages = new HashSet(); + + if (rootPackage != null) + { + foreach (var dep in rootPackage.MetadataRequiresDist) + { + explicitPackages.Add(dep.Name); + } + + foreach (var devDep in rootPackage.MetadataRequiresDev) + { + devPackages.Add(devDep.Name); + } + } + + foreach (var pkg in uvLock.Packages) + { + if (IsRootPackage(pkg)) + { + continue; + } + + var pipComponent = new PipComponent(pkg.Name, pkg.Version); + var isExplicit = explicitPackages.Contains(pkg.Name); + var isDev = devPackages.Contains(pkg.Name); + var detectedComponent = new DetectedComponent(pipComponent); + singleFileComponentRecorder.RegisterUsage(detectedComponent, isDevelopmentDependency: isDev, isExplicitReferencedDependency: isExplicit); + + foreach (var dep in pkg.Dependencies) + { + var depPkg = uvLock.Packages.FirstOrDefault(p => p.Name.Equals(dep.Name, StringComparison.OrdinalIgnoreCase)); + if (depPkg != null) + { + var depComponentWithVersion = new PipComponent(depPkg.Name, depPkg.Version); + singleFileComponentRecorder.RegisterUsage(new DetectedComponent(depComponentWithVersion), parentComponentId: pipComponent.Id); + } + else + { + this.Logger.LogWarning("Dependency {DependencyName} not found in uv.lock packages", dep.Name); + } + } + } + } + catch (Exception ex) + { + this.Logger.LogError(ex, "Failed to parse uv.lock file {File}", file.Location); + } + + return Task.CompletedTask; + } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvPackage.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvPackage.cs new file mode 100644 index 000000000..1e7e4a33d --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvPackage.cs @@ -0,0 +1,23 @@ +#nullable enable +namespace Microsoft.ComponentDetection.Detectors.Uv +{ + using System.Collections.Generic; + + public class UvPackage + { + public required string Name { get; init; } + + public required string Version { get; init; } + + public List Dependencies { get; set; } = []; + + // Metadata dependencies (requires-dist) + public List MetadataRequiresDist { get; set; } = []; + + // Metadata dev dependencies (requires-dev) + public List MetadataRequiresDev { get; set; } = []; + + // Source property for uv.lock + public UvSource? Source { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvSource.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvSource.cs new file mode 100644 index 000000000..6c06b3625 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvSource.cs @@ -0,0 +1,11 @@ +#nullable enable + +namespace Microsoft.ComponentDetection.Detectors.Uv +{ + public class UvSource + { + public string? Registry { get; set; } + + public string? Virtual { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/UvLockDetectorExperiment.cs b/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/UvLockDetectorExperiment.cs new file mode 100644 index 000000000..c5053e4d8 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/UvLockDetectorExperiment.cs @@ -0,0 +1,23 @@ +namespace Microsoft.ComponentDetection.Orchestrator.Experiments.Configs; + +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Detectors.Pip; +using Microsoft.ComponentDetection.Detectors.Uv; + +/// +/// Experiment to validate UvLockComponentDetector against PipComponentDetector. +/// +public class UvLockDetectorExperiment : IExperimentConfiguration +{ + /// + public string Name => "UvLockDetectorExperiment"; + + /// + public bool IsInControlGroup(IComponentDetector componentDetector) => componentDetector is PipReportComponentDetector; + + /// + public bool IsInExperimentGroup(IComponentDetector componentDetector) => componentDetector is UvLockComponentDetector; + + /// + public bool ShouldRecord(IComponentDetector componentDetector, int numComponents) => true; +} diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs b/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs index c91dee846..8b61b57db 100644 --- a/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs +++ b/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs @@ -21,6 +21,7 @@ namespace Microsoft.ComponentDetection.Orchestrator.Extensions; using Microsoft.ComponentDetection.Detectors.Rust; using Microsoft.ComponentDetection.Detectors.Spdx; using Microsoft.ComponentDetection.Detectors.Swift; +using Microsoft.ComponentDetection.Detectors.Uv; using Microsoft.ComponentDetection.Detectors.Vcpkg; using Microsoft.ComponentDetection.Detectors.Yarn; using Microsoft.ComponentDetection.Detectors.Yarn.Parsers; @@ -67,6 +68,7 @@ public static IServiceCollection AddComponentDetection(this IServiceCollection s services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // Detectors // CocoaPods @@ -152,6 +154,9 @@ public static IServiceCollection AddComponentDetection(this IServiceCollection s // Swift Package Manager services.AddSingleton(); + // uv + services.AddSingleton(); + return services; } } diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/Microsoft.ComponentDetection.Detectors.Tests.csproj b/test/Microsoft.ComponentDetection.Detectors.Tests/Microsoft.ComponentDetection.Detectors.Tests.csproj index 2b9b4a22a..03644cbfe 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/Microsoft.ComponentDetection.Detectors.Tests.csproj +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/Microsoft.ComponentDetection.Detectors.Tests.csproj @@ -14,6 +14,7 @@ + diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs new file mode 100644 index 000000000..19cf0cc7a --- /dev/null +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockDetectorTests.cs @@ -0,0 +1,269 @@ +namespace Microsoft.ComponentDetection.Detectors.Tests; + +using System; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.TypedComponent; +using Microsoft.ComponentDetection.Detectors.Uv; +using Microsoft.ComponentDetection.TestsUtilities; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +[TestClass] +[TestCategory("Governance/All")] +[TestCategory("Governance/ComponentDetection")] +public class UvLockDetectorTests : BaseDetectorTest +{ + [TestMethod] + public async Task TestUvLockDetectorWithNoFiles_ReturnsSuccessfullyAsync() + { + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Should().BeEmpty(); + } + + [TestMethod] + public async Task TestUvLockDetectorWithEmptyLockFile_FindsNothingAsync() + { + var emptyUvLock = string.Empty; // Empty TOML + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("uv.lock", emptyUvLock) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Should().BeEmpty(); + } + + [TestMethod] + public async Task TestUvLockDetectorWithNoPackages_FindsNothingAsync() + { + var uvLock = "# uv.lock file\n[metadata]\nversion = '1'\n"; + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("uv.lock", uvLock) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Should().BeEmpty(); + } + + [TestMethod] + public async Task TestUvLockDetectorWithMultiplePackages_FindsAllComponentsAndGraphAsync() + { + var uvLock = @" +[[package]] +name = 'foo' +version = '1.2.3' +[[package]] +name = 'bar' +version = '4.5.6' +"; + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("uv.lock", uvLock) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + var detectedComponents = componentRecorder.GetDetectedComponents(); + detectedComponents.Should().HaveCount(2); + detectedComponents.Select(x => ((PipComponent)x.Component).Name).Should().BeEquivalentTo(["foo", "bar"]); + detectedComponents.Select(x => ((PipComponent)x.Component).Version).Should().BeEquivalentTo(["1.2.3", "4.5.6"]); + + // Validate dependency graph structure: both are roots, no dependencies + var graphs = componentRecorder.GetDependencyGraphsByLocation(); + var graphKey = graphs.Keys.FirstOrDefault(k => k.EndsWith("uv.lock")); + graphKey.Should().NotBeNull(); + var graph = graphs[graphKey]; + var fooId = new PipComponent("foo", "1.2.3").Id; + var barId = new PipComponent("bar", "4.5.6").Id; + graph.GetComponents().Should().BeEquivalentTo([fooId, barId]); + graph.GetDependenciesForComponent(fooId).Should().BeEmpty(); + graph.GetDependenciesForComponent(barId).Should().BeEmpty(); + } + + [TestMethod] + public async Task TestUvLockDetectorWithDependencies_RegistersDependenciesAsync() + { + var uvLock = @" +[[package]] +name = 'foo' +version = '1.2.3' +dependencies = [{ name = 'bar' }] +[[package]] +name = 'bar' +version = '4.5.6' +"; + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("uv.lock", uvLock) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + var detectedComponents = componentRecorder.GetDetectedComponents(); + detectedComponents.Should().HaveCount(2); + + var fooId = new PipComponent("foo", "1.2.3").Id; + var barId = new PipComponent("bar", "4.5.6").Id; + + var graphs = componentRecorder.GetDependencyGraphsByLocation(); + var graphKey = graphs.Keys.FirstOrDefault(k => k.EndsWith("uv.lock")); + var graph = graphs[graphKey]; + + graph.GetComponents().Should().BeEquivalentTo([fooId, barId]); + graph.GetDependenciesForComponent(fooId).Should().BeEquivalentTo([barId]); + graph.GetDependenciesForComponent(barId).Should().BeEmpty(); + } + + [TestMethod] + public async Task TestUvLockDetectorWithMissingDependency_LogsWarningAsync() + { + var loggerMock = new Mock>(); + this.DetectorTestUtility.AddServiceMock(loggerMock); + + var uvLock = @" +[[package]] +name = 'foo' +version = '1.2.3' +dependencies = [{ name = 'baz' }] +[[package]] +name = 'bar' +version = '4.5.6' +"; + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("uv.lock", uvLock) + .AddServiceMock(loggerMock) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + + // Should log a warning for missing dependency 'baz' + loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Dependency baz not found")), + null, + It.IsAny>()), + Times.AtLeastOnce()); + } + + [TestMethod] + public async Task TestUvLockDetectorWithMalformedPackage_IgnoresInvalidAsync() + { + var uvLock = @" +[[package]] +name = 'foo' +[[package]] +version = '4.5.6' +"; + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("uv.lock", uvLock) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + var detectedComponents = componentRecorder.GetDetectedComponents(); + detectedComponents.Should().BeEmpty(); + } + + [TestMethod] + public async Task TestUvLockDetectorWithInvalidFile_LogsErrorAsync() + { + var loggerMock = new Mock>(); + this.DetectorTestUtility.AddServiceMock(loggerMock); + + var invalidUvLock = "not a valid toml file"; + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("uv.lock", invalidUvLock) + .AddServiceMock(loggerMock) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + + // Should log an error for parse failure + loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Failed to parse uv.lock file")), + It.IsAny(), + It.IsAny>()), + Times.AtLeastOnce()); + } + + [TestMethod] + public async Task TestUvLockDetector_ExplicitDependencies_AreMarkedExplicit() + { + var uvLock = """ +[[package]] +name = 'foo' +version = '1.2.3' +source = { virtual = '.' } + +[package.metadata] +requires-dist = [ + { name = "bar", specifier = ">=3.9.1" }, +] + +[[package]] +name = 'bar' +version = '2.0.0' +"""; + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("uv.lock", uvLock) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + var detected = componentRecorder.GetDetectedComponents().ToList(); + var graph = componentRecorder.GetDependencyGraphsByLocation().Values.First(); + + detected.Should().ContainSingle(); + var barId = detected.First(d => d.Component.Id.StartsWith("bar")).Component.Id; + graph.IsComponentExplicitlyReferenced(barId).Should().BeTrue(); + } + + [TestMethod] + public async Task TestUvLockDetector_DevelopmentAndNonDevelopmentDependencies() + { + var uvLock = @"[[package]] +name = 'foo' +version = '1.2.3' +source = { virtual = '.' } +[package.metadata] +requires-dist = [ + { name = 'bar', specifier = '>=2.0.0' }, + { name = 'baz', specifier = '>=3.0.0' } +] +[package.metadata.requires-dev] +dev = [ + { name = 'devonly', specifier = '>=4.0.0' } +] +[[package]] +name = 'bar' +version = '2.0.0' +[[package]] +name = 'baz' +version = '3.0.0' +[[package]] +name = 'devonly' +version = '4.0.0' +"; + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("uv.lock", uvLock) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + var detected = componentRecorder.GetDetectedComponents().ToList(); + var graph = componentRecorder.GetDependencyGraphsByLocation().Values.First(); + + var barId = detected.First(d => d.Component.Id.StartsWith("bar ")).Component.Id; + var bazId = detected.First(d => d.Component.Id.StartsWith("baz ")).Component.Id; + var devonlyId = new PipComponent("devonly", "4.0.0").Id; + + // bar and baz are non-dev dependencies, devonly is a dev dependency + graph.IsDevelopmentDependency(barId).Should().BeFalse(); + graph.IsDevelopmentDependency(bazId).Should().BeFalse(); + graph.IsDevelopmentDependency(devonlyId).Should().BeTrue(); + } +} diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs new file mode 100644 index 000000000..fea14df3d --- /dev/null +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs @@ -0,0 +1,395 @@ +namespace Microsoft.ComponentDetection.Detectors.Tests +{ + using System; + using System.IO; + using System.Linq; + using System.Text; + using FluentAssertions; + using Microsoft.ComponentDetection.Detectors.Uv; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using Tomlyn.Model; + + [TestClass] + public class UvLockTests + { + [TestMethod] + public void Parse_ParsesMetadataRequiresDistAndDev() + { + var toml = """ +[[package]] +name = "component-detection" +version = "0.0.0" + +[package.metadata] +requires-dist = [ + { name = "azure-identity", specifier = "==1.17.1" }, + { name = "flask", specifier = ">2,<3" }, + { name = "requests", specifier = ">=2.32.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8.3.4" }, + { name = "pytest-cov", specifier = ">=6.0.0" }, + { name = "pytest-env", specifier = ">=1.1.5" }, +] +"""; + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().ContainSingle(); + + var package = uvLock.Packages.First(); + package.Name.Should().Be("component-detection"); + package.Version.Should().Be("0.0.0"); + + package.MetadataRequiresDist.Should().BeEquivalentTo( + [ + new UvDependency { Name = "azure-identity", Specifier = "==1.17.1" }, + new UvDependency { Name = "flask", Specifier = ">2,<3" }, + new UvDependency { Name = "requests", Specifier = ">=2.32.0" }, + ], + options => options.ComparingByMembers()); + + package.MetadataRequiresDev.Should().BeEquivalentTo( + [ + new UvDependency { Name = "pytest", Specifier = ">=8.3.4" }, + new UvDependency { Name = "pytest-cov", Specifier = ">=6.0.0" }, + new UvDependency { Name = "pytest-env", Specifier = ">=1.1.5" }, + ], + options => options.ComparingByMembers()); + } + + [TestMethod] + public void Parse_ParsesPackagesAndDependencies() + { + var toml = @" +[[package]] +name = 'foo' +version = '1.2.3' +dependencies = [ + { name = 'bar', specifier = '>=2.0.0' }, +] +[[package]] +name = 'bar' +version = '2.0.0' +"; + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().HaveCount(2); + uvLock.Packages.First().Name.Should().Be("foo"); + uvLock.Packages.First().Dependencies.Should().ContainSingle(d => d.Name == "bar" && d.Specifier == ">=2.0.0"); + } + + [TestMethod] + public void Parse_EmptyStream_ReturnsNoPackages() + { + using var ms = new MemoryStream([]); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().BeEmpty(); + } + + [TestMethod] + public void Parse_TomlNotATable_ThrowsException() + { + var toml = "42"; + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + FluentActions.Invoking(() => UvLock.Parse(ms)) + .Should().Throw(); + } + + [TestMethod] + public void Parse_NoPackageKey_ReturnsNoPackages() + { + var toml = "[metadata]\nversion = '1'"; + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().BeEmpty(); + } + + [TestMethod] + public void Parse_PackageKeyNotArray_ReturnsNoPackages() + { + var toml = "package = 42"; + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().BeEmpty(); + } + + [TestMethod] + public void Parse_PackageMissingNameOrVersion_IgnoresPackage() + { + var toml = @" +[[package]] +version = '1.2.3' +[[package]] +name = 'foo' +"; + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().BeEmpty(); + } + + [TestMethod] + public void Parse_PackageWithMalformedDependencies_IgnoresMalformed() + { + var toml = @" +[[package]] +name = 'foo' +version = '1.2.3' +dependencies = [42, { name = 'bar' }] +"; + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().ContainSingle(); + var pkg = uvLock.Packages.First(); + pkg.Dependencies.Should().ContainSingle(d => d.Name == "bar"); + } + + [TestMethod] + public void Parse_PackageWithMalformedMetadata_IgnoresMalformed() + { + var toml = @" +[[package]] +name = 'foo' +version = '1.2.3' +[package.metadata] +requires-dist = [42, { name = 'bar' }] +[package.metadata.requires-dev] +dev = [42, { name = 'baz' }] +"; + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().ContainSingle(); + var pkg = uvLock.Packages.First(); + pkg.MetadataRequiresDist.Should().ContainSingle(d => d.Name == "bar"); + pkg.MetadataRequiresDev.Should().ContainSingle(d => d.Name == "baz"); + } + + [TestMethod] + public void ParsePackagesFromModel_InvalidRoot_Throws() + { + Action act = () => UvLock.ParsePackagesFromModel(42); + act.Should().Throw(); + } + + [TestMethod] + public void ParsePackagesFromModel_NoPackages_ReturnsEmpty() + { + var table = new TomlTable(); + var result = UvLock.ParsePackagesFromModel(table); + result.Should().BeEmpty(); + } + + [TestMethod] + public void ParsePackage_ValidPackage_ParsesCorrectly() + { + var pkg = new TomlTable + { + ["name"] = "foo", + ["version"] = "1.0.0", + ["dependencies"] = new TomlArray { new TomlTable { ["name"] = "bar", ["specifier"] = ">=2.0.0" } }, + }; + var result = UvLock.ParsePackage(pkg); + result.Should().NotBeNull(); + result.Name.Should().Be("foo"); + result.Version.Should().Be("1.0.0"); + result.Dependencies.Should().ContainSingle(d => d.Name == "bar" && d.Specifier == ">=2.0.0"); + } + + [TestMethod] + public void ParsePackage_MissingNameOrVersion_ReturnsNull() + { + var pkg1 = new TomlTable { ["version"] = "1.0.0" }; + var pkg2 = new TomlTable { ["name"] = "foo" }; + UvLock.ParsePackage(pkg1).Should().BeNull(); + UvLock.ParsePackage(pkg2).Should().BeNull(); + } + + [TestMethod] + public void ParsePackage_NullOrNonTable_ReturnsNull() + { + UvLock.ParsePackage(null).Should().BeNull(); + UvLock.ParsePackage(42).Should().BeNull(); + } + + [TestMethod] + public void ParsePackage_BranchCoverage_AllPaths() + { + // Path: pkg is TomlTable, but missing name + var pkgMissingName = new TomlTable { ["version"] = "1.0.0" }; + UvLock.ParsePackage(pkgMissingName).Should().BeNull(); + + // Path: pkg is TomlTable, but missing version + var pkgMissingVersion = new TomlTable { ["name"] = "foo" }; + UvLock.ParsePackage(pkgMissingVersion).Should().BeNull(); + } + + [TestMethod] + public void ParseDependenciesArray_ParsesValidDepsAndSkipsMalformed() + { + var arr = new TomlArray { 42, new TomlTable { ["name"] = "bar", ["specifier"] = "==1.2.3" }, new TomlTable { ["name"] = "baz" } }; + var result = UvLock.ParseDependenciesArray(arr); + result.Should().Contain(d => d.Name == "bar" && d.Specifier == "==1.2.3"); + result.Should().Contain(d => d.Name == "baz" && d.Specifier == null); + result.Should().HaveCount(2); + } + + [TestMethod] + public void ParseDependenciesArray_NullOrNoValidDeps_ReturnsEmpty() + { + UvLock.ParseDependenciesArray(null).Should().BeEmpty(); + var arr = new TomlArray { 42, "foo", 3.14 }; + UvLock.ParseDependenciesArray(arr).Should().BeEmpty(); + } + + [TestMethod] + public void ParseDependenciesArray_BranchCoverage_AllPaths() + { + // Path: dep is TomlTable but missing name + var arr = new TomlArray { new TomlTable { ["specifier"] = "==1.2.3" } }; + UvLock.ParseDependenciesArray(arr).Should().BeEmpty(); + } + + [TestMethod] + public void ParseMetadata_ParsesRequiresDistAndDev() + { + var pkg = new UvPackage { Name = "foo", Version = "1.0.0" }; + var metadata = new TomlTable + { + ["requires-dist"] = new TomlArray { new TomlTable { ["name"] = "bar", ["specifier"] = ">=2.0.0" } }, + ["requires-dev"] = new TomlTable { ["dev"] = new TomlArray { new TomlTable { ["name"] = "baz" } } }, + }; + UvLock.ParseMetadata(metadata, pkg); + pkg.MetadataRequiresDist.Should().ContainSingle(d => d.Name == "bar" && d.Specifier == ">=2.0.0"); + pkg.MetadataRequiresDev.Should().ContainSingle(d => d.Name == "baz" && d.Specifier == null); + } + + [TestMethod] + public void ParseMetadata_NullOrNoRelevantKeys_DoesNothing() + { + var pkg = new UvPackage { Name = "foo", Version = "1.0.0" }; + UvLock.ParseMetadata(null, pkg); // Should not throw + var emptyTable = new TomlTable(); + UvLock.ParseMetadata(emptyTable, pkg); // Should not throw or set anything + pkg.MetadataRequiresDist.Should().BeEmpty(); + pkg.MetadataRequiresDev.Should().BeEmpty(); + } + + [TestMethod] + public void ParseMetadata_BranchCoverage_RequiresDistOnly() + { + var pkg = new UvPackage { Name = "foo", Version = "1.0.0" }; + var metadata = new TomlTable + { + ["requires-dist"] = new TomlArray { new TomlTable { ["name"] = "bar" } }, + }; + UvLock.ParseMetadata(metadata, pkg); + pkg.MetadataRequiresDist.Should().ContainSingle(d => d.Name == "bar"); + pkg.MetadataRequiresDev.Should().BeEmpty(); + } + + [TestMethod] + public void ParseMetadata_BranchCoverage_RequiresDevOnly() + { + var pkg = new UvPackage { Name = "foo", Version = "1.0.0" }; + var metadata = new TomlTable + { + ["requires-dev"] = new TomlTable { ["dev"] = new TomlArray { new TomlTable { ["name"] = "baz" } } }, + }; + UvLock.ParseMetadata(metadata, pkg); + pkg.MetadataRequiresDist.Should().BeEmpty(); + pkg.MetadataRequiresDev.Should().ContainSingle(d => d.Name == "baz"); + } + + [TestMethod] + public void ParseMetadata_RequiresDevTableWithoutDevArray_DoesNotThrowOrSet() + { + var pkg = new UvPackage { Name = "foo", Version = "1.0.0" }; + + // requires-dev exists but no "dev" key + var metadata = new TomlTable + { + ["requires-dev"] = new TomlTable { ["notdev"] = 42 }, + }; + UvLock.ParseMetadata(metadata, pkg); + pkg.MetadataRequiresDev.Should().BeEmpty(); + + // requires-dev exists, "dev" is not a TomlArray + metadata = new TomlTable + { + ["requires-dev"] = new TomlTable { ["dev"] = 42 }, + }; + UvLock.ParseMetadata(metadata, pkg); + pkg.MetadataRequiresDev.Should().BeEmpty(); + } + + [TestMethod] + public void ParsePackage_ParsesSourceRegistryAndVirtual() + { + var toml = """ +[[package]] +name = 'foo' +version = '1.0.0' +source = { registry = 'https://example.com/', virtual = '.' } +"""; + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().ContainSingle(); + var pkg = uvLock.Packages.First(); + pkg.Source.Should().NotBeNull(); + pkg.Source!.Registry.Should().Be("https://example.com/"); + pkg.Source.Virtual.Should().Be("."); + } + + [TestMethod] + public void ParsePackage_ParsesSource_RegistryOnly() + { + var toml = """ +[[package]] +name = 'foo' +version = '1.0.0' +source = { registry = 'https://example.com/' } +"""; + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().ContainSingle(); + var pkg = uvLock.Packages.First(); + pkg.Source.Should().NotBeNull(); + pkg.Source!.Registry.Should().Be("https://example.com/"); + pkg.Source.Virtual.Should().BeNull(); + } + + [TestMethod] + public void ParsePackage_ParsesSource_VirtualOnly() + { + var toml = """ +[[package]] +name = 'foo' +version = '1.0.0' +source = { virtual = '.' } +"""; + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().ContainSingle(); + var pkg = uvLock.Packages.First(); + pkg.Source.Should().NotBeNull(); + pkg.Source!.Registry.Should().BeNull(); + pkg.Source.Virtual.Should().Be("."); + } + + [TestMethod] + public void ParsePackage_ParsesSource_Missing() + { + var toml = """ +[[package]] +name = 'foo' +version = '1.0.0' +"""; + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().ContainSingle(); + var pkg = uvLock.Packages.First(); + pkg.Source.Should().BeNull(); + } + } +} diff --git a/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/UvLockDetectorExperimentTests.cs b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/UvLockDetectorExperimentTests.cs new file mode 100644 index 000000000..a3d78c9b9 --- /dev/null +++ b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/UvLockDetectorExperimentTests.cs @@ -0,0 +1,41 @@ +namespace Microsoft.ComponentDetection.Orchestrator.Tests.Experiments; + +using FluentAssertions; +using Microsoft.ComponentDetection.Detectors.Pip; +using Microsoft.ComponentDetection.Detectors.Uv; +using Microsoft.ComponentDetection.Orchestrator.Experiments.Configs; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class UvLockDetectorExperimentTests +{ + private readonly UvLockDetectorExperiment experiment = new(); + + [TestMethod] + public void IsInControlGroup_ReturnsTrue_ForPipReportComponentDetector() + { + var pipReportDetector = new PipReportComponentDetector(null, null, null, null, null, null, null, null, null); + this.experiment.IsInControlGroup(pipReportDetector).Should().BeTrue(); + } + + [TestMethod] + public void IsInControlGroup_ReturnsFalse_ForPipComponentDetector() + { + var pipDetector = new PipComponentDetector(null, null, null, null, null); + this.experiment.IsInControlGroup(pipDetector).Should().BeFalse(); + } + + [TestMethod] + public void IsInExperimentGroup_ReturnsTrue_ForUvLockComponentDetector() + { + var uvLockDetector = new UvLockComponentDetector(null, null, null); + this.experiment.IsInExperimentGroup(uvLockDetector).Should().BeTrue(); + } + + [TestMethod] + public void ShouldRecord_AlwaysReturnsTrue() + { + var pipDetector = new PipComponentDetector(null, null, null, null, null); + this.experiment.ShouldRecord(pipDetector, 0).Should().BeTrue(); + } +} diff --git a/test/Microsoft.ComponentDetection.VerificationTests/resources/uv/pyproject.toml b/test/Microsoft.ComponentDetection.VerificationTests/resources/uv/pyproject.toml new file mode 100644 index 000000000..f79d996bb --- /dev/null +++ b/test/Microsoft.ComponentDetection.VerificationTests/resources/uv/pyproject.toml @@ -0,0 +1,18 @@ +[project] +name = "component-detection" +version = "0.0.0" +description = "A test project" +authors = [] +requires-python = "==3.9.*" +dependencies = [ + "azure-identity==1.17.1", + "flask>2,<3", + "requests>=2.32.0", +] + +[dependency-groups] +dev = [ + "pytest-env>=1.1.5", + "pytest>=8.3.4", + "pytest-cov>=6.0.0" +] diff --git a/test/Microsoft.ComponentDetection.VerificationTests/resources/uv/uv.lock b/test/Microsoft.ComponentDetection.VerificationTests/resources/uv/uv.lock new file mode 100644 index 000000000..c377086ca --- /dev/null +++ b/test/Microsoft.ComponentDetection.VerificationTests/resources/uv/uv.lock @@ -0,0 +1,497 @@ +version = 1 +requires-python = "==3.9.*" + +[[package]] +name = "azure-core" +version = "1.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, + { name = "six" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/29/ff7a519a315e41c85bab92a7478c6acd1cf0b14353139a08caee4c691f77/azure_core-1.34.0.tar.gz", hash = "sha256:bdb544989f246a0ad1c85d72eeb45f2f835afdcbc5b45e43f0dbde7461c81ece", size = 297999 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/9e/5c87b49f65bb16571599bc789857d0ded2f53014d3392bc88a5d1f3ad779/azure_core-1.34.0-py3-none-any.whl", hash = "sha256:0615d3b756beccdb6624d1c0ae97284f38b78fb59a2a9839bf927c66fbbdddd6", size = 207409 }, +] + +[[package]] +name = "azure-identity" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "cryptography" }, + { name = "msal" }, + { name = "msal-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/c9/f7e3926686a89670ce641b360bd2da9a2d7a12b3e532403462d99f81e9d5/azure-identity-1.17.1.tar.gz", hash = "sha256:32ecc67cc73f4bd0595e4f64b1ca65cd05186f4fe6f98ed2ae9f1aa32646efea", size = 246652 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/83/a777861351e7b99e7c84ff3b36bab35e87b6e5d36e50b6905e148c696515/azure_identity-1.17.1-py3-none-any.whl", hash = "sha256:db8d59c183b680e763722bfe8ebc45930e6c57df510620985939f7f3191e0382", size = 173229 }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458 }, +] + +[[package]] +name = "certifi" +version = "2025.4.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618 }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/ea/8bb50596b8ffbc49ddd7a1ad305035daa770202a6b782fc164647c2673ad/cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", size = 182220 }, + { url = "https://files.pythonhosted.org/packages/ae/11/e77c8cd24f58285a82c23af484cf5b124a376b32644e445960d1a4654c3a/cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", size = 178605 }, + { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910 }, + { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200 }, + { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565 }, + { url = "https://files.pythonhosted.org/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635 }, + { url = "https://files.pythonhosted.org/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218 }, + { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486 }, + { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911 }, + { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632 }, + { url = "https://files.pythonhosted.org/packages/cb/b5/fd9f8b5a84010ca169ee49f4e4ad6f8c05f4e3545b72ee041dbbcb159882/cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", size = 171820 }, + { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/f8/dfb01ff6cc9af38552c69c9027501ff5a5117c4cc18dcd27cb5259fa1888/charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", size = 201671 }, + { url = "https://files.pythonhosted.org/packages/32/fb/74e26ee556a9dbfe3bd264289b67be1e6d616329403036f6507bb9f3f29c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", size = 144744 }, + { url = "https://files.pythonhosted.org/packages/ad/06/8499ee5aa7addc6f6d72e068691826ff093329fe59891e83b092ae4c851c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", size = 154993 }, + { url = "https://files.pythonhosted.org/packages/f1/a2/5e4c187680728219254ef107a6949c60ee0e9a916a5dadb148c7ae82459c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", size = 147382 }, + { url = "https://files.pythonhosted.org/packages/4c/fe/56aca740dda674f0cc1ba1418c4d84534be51f639b5f98f538b332dc9a95/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", size = 149536 }, + { url = "https://files.pythonhosted.org/packages/53/13/db2e7779f892386b589173dd689c1b1e304621c5792046edd8a978cbf9e0/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", size = 151349 }, + { url = "https://files.pythonhosted.org/packages/69/35/e52ab9a276186f729bce7a0638585d2982f50402046e4b0faa5d2c3ef2da/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", size = 146365 }, + { url = "https://files.pythonhosted.org/packages/a6/d8/af7333f732fc2e7635867d56cb7c349c28c7094910c72267586947561b4b/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", size = 154499 }, + { url = "https://files.pythonhosted.org/packages/7a/3d/a5b2e48acef264d71e036ff30bcc49e51bde80219bb628ba3e00cf59baac/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", size = 157735 }, + { url = "https://files.pythonhosted.org/packages/85/d8/23e2c112532a29f3eef374375a8684a4f3b8e784f62b01da931186f43494/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", size = 154786 }, + { url = "https://files.pythonhosted.org/packages/c7/57/93e0169f08ecc20fe82d12254a200dfaceddc1c12a4077bf454ecc597e33/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", size = 150203 }, + { url = "https://files.pythonhosted.org/packages/2c/9d/9bf2b005138e7e060d7ebdec7503d0ef3240141587651f4b445bdf7286c2/charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", size = 98436 }, + { url = "https://files.pythonhosted.org/packages/6d/24/5849d46cf4311bbf21b424c443b09b459f5b436b1558c04e45dbb7cc478b/charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", size = 105772 }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, +] + +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "component-detection" +version = "0.0.0" +source = { virtual = "." } +dependencies = [ + { name = "azure-identity" }, + { name = "flask" }, + { name = "requests" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-env" }, +] + +[package.metadata] +requires-dist = [ + { name = "azure-identity", specifier = "==1.17.1" }, + { name = "flask", specifier = ">2,<3" }, + { name = "requests", specifier = ">=2.32.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8.3.4" }, + { name = "pytest-cov", specifier = ">=6.0.0" }, + { name = "pytest-env", specifier = ">=1.1.5" }, +] + +[[package]] +name = "coverage" +version = "7.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/07/998afa4a0ecdf9b1981ae05415dad2d4e7716e1b1f00abbd91691ac09ac9/coverage-7.8.2.tar.gz", hash = "sha256:a886d531373a1f6ff9fad2a2ba4a045b68467b779ae729ee0b3b10ac20033b27", size = 812759 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/1e/388267ad9c6aa126438acc1ceafede3bb746afa9872e3ec5f0691b7d5efa/coverage-7.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:496948261eaac5ac9cf43f5d0a9f6eb7a6d4cb3bedb2c5d294138142f5c18f2a", size = 211566 }, + { url = "https://files.pythonhosted.org/packages/8f/a5/acc03e5cf0bba6357f5e7c676343de40fbf431bb1e115fbebf24b2f7f65e/coverage-7.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eacd2de0d30871eff893bab0b67840a96445edcb3c8fd915e6b11ac4b2f3fa6d", size = 211996 }, + { url = "https://files.pythonhosted.org/packages/5b/a2/0fc0a9f6b7c24fa4f1d7210d782c38cb0d5e692666c36eaeae9a441b6755/coverage-7.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b039ffddc99ad65d5078ef300e0c7eed08c270dc26570440e3ef18beb816c1ca", size = 240741 }, + { url = "https://files.pythonhosted.org/packages/e6/da/1c6ba2cf259710eed8916d4fd201dccc6be7380ad2b3b9f63ece3285d809/coverage-7.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e49824808d4375ede9dd84e9961a59c47f9113039f1a525e6be170aa4f5c34d", size = 238672 }, + { url = "https://files.pythonhosted.org/packages/ac/51/c8fae0dc3ca421e6e2509503696f910ff333258db672800c3bdef256265a/coverage-7.8.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b069938961dfad881dc2f8d02b47645cd2f455d3809ba92a8a687bf513839787", size = 239769 }, + { url = "https://files.pythonhosted.org/packages/59/8e/b97042ae92c59f40be0c989df090027377ba53f2d6cef73c9ca7685c26a6/coverage-7.8.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:de77c3ba8bb686d1c411e78ee1b97e6e0b963fb98b1637658dd9ad2c875cf9d7", size = 239555 }, + { url = "https://files.pythonhosted.org/packages/47/35/b8893e682d6e96b1db2af5997fc13ef62219426fb17259d6844c693c5e00/coverage-7.8.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1676628065a498943bd3f64f099bb573e08cf1bc6088bbe33cf4424e0876f4b3", size = 237768 }, + { url = "https://files.pythonhosted.org/packages/03/6c/023b0b9a764cb52d6243a4591dcb53c4caf4d7340445113a1f452bb80591/coverage-7.8.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8e1a26e7e50076e35f7afafde570ca2b4d7900a491174ca357d29dece5aacee7", size = 238757 }, + { url = "https://files.pythonhosted.org/packages/03/ed/3af7e4d721bd61a8df7de6de9e8a4271e67f3d9e086454558fd9f48eb4f6/coverage-7.8.2-cp39-cp39-win32.whl", hash = "sha256:6782a12bf76fa61ad9350d5a6ef5f3f020b57f5e6305cbc663803f2ebd0f270a", size = 214166 }, + { url = "https://files.pythonhosted.org/packages/9d/30/ee774b626773750dc6128354884652507df3c59d6aa8431526107e595227/coverage-7.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:1efa4166ba75ccefd647f2d78b64f53f14fb82622bc94c5a5cb0a622f50f1c9e", size = 215050 }, + { url = "https://files.pythonhosted.org/packages/69/2f/572b29496d8234e4a7773200dd835a0d32d9e171f2d974f3fe04a9dbc271/coverage-7.8.2-pp39.pp310.pp311-none-any.whl", hash = "sha256:ec455eedf3ba0bbdf8f5a570012617eb305c63cb9f03428d39bf544cb2b94837", size = 203636 }, + { url = "https://files.pythonhosted.org/packages/a0/1a/0b9c32220ad694d66062f571cc5cedfa9997b64a591e8a500bb63de1bd40/coverage-7.8.2-py3-none-any.whl", hash = "sha256:726f32ee3713f7359696331a18daf0c3b3a70bb0ae71141b9d3c52be7c595e32", size = 203623 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli" }, +] + +[[package]] +name = "cryptography" +version = "45.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/1f/9fa001e74a1993a9cadd2333bb889e50c66327b8594ac538ab8a04f915b7/cryptography-45.0.3.tar.gz", hash = "sha256:ec21313dd335c51d7877baf2972569f40a4291b76a0ce51391523ae358d05899", size = 744738 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/b2/2345dc595998caa6f68adf84e8f8b50d18e9fc4638d32b22ea8daedd4b7a/cryptography-45.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:7573d9eebaeceeb55285205dbbb8753ac1e962af3d9640791d12b36864065e71", size = 7056239 }, + { url = "https://files.pythonhosted.org/packages/71/3d/ac361649a0bfffc105e2298b720d8b862330a767dab27c06adc2ddbef96a/cryptography-45.0.3-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d377dde61c5d67eb4311eace661c3efda46c62113ff56bf05e2d679e02aebb5b", size = 4205541 }, + { url = "https://files.pythonhosted.org/packages/70/3e/c02a043750494d5c445f769e9c9f67e550d65060e0bfce52d91c1362693d/cryptography-45.0.3-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fae1e637f527750811588e4582988932c222f8251f7b7ea93739acb624e1487f", size = 4433275 }, + { url = "https://files.pythonhosted.org/packages/40/7a/9af0bfd48784e80eef3eb6fd6fde96fe706b4fc156751ce1b2b965dada70/cryptography-45.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ca932e11218bcc9ef812aa497cdf669484870ecbcf2d99b765d6c27a86000942", size = 4209173 }, + { url = "https://files.pythonhosted.org/packages/31/5f/d6f8753c8708912df52e67969e80ef70b8e8897306cd9eb8b98201f8c184/cryptography-45.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af3f92b1dc25621f5fad065288a44ac790c5798e986a34d393ab27d2b27fcff9", size = 3898150 }, + { url = "https://files.pythonhosted.org/packages/8b/50/f256ab79c671fb066e47336706dc398c3b1e125f952e07d54ce82cf4011a/cryptography-45.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2f8f8f0b73b885ddd7f3d8c2b2234a7d3ba49002b0223f58cfde1bedd9563c56", size = 4466473 }, + { url = "https://files.pythonhosted.org/packages/62/e7/312428336bb2df0848d0768ab5a062e11a32d18139447a76dfc19ada8eed/cryptography-45.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9cc80ce69032ffa528b5e16d217fa4d8d4bb7d6ba8659c1b4d74a1b0f4235fca", size = 4211890 }, + { url = "https://files.pythonhosted.org/packages/e7/53/8a130e22c1e432b3c14896ec5eb7ac01fb53c6737e1d705df7e0efb647c6/cryptography-45.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c824c9281cb628015bfc3c59335163d4ca0540d49de4582d6c2637312907e4b1", size = 4466300 }, + { url = "https://files.pythonhosted.org/packages/ba/75/6bb6579688ef805fd16a053005fce93944cdade465fc92ef32bbc5c40681/cryptography-45.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5833bb4355cb377ebd880457663a972cd044e7f49585aee39245c0d592904578", size = 4332483 }, + { url = "https://files.pythonhosted.org/packages/2f/11/2538f4e1ce05c6c4f81f43c1ef2bd6de7ae5e24ee284460ff6c77e42ca77/cryptography-45.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bb5bf55dcb69f7067d80354d0a348368da907345a2c448b0babc4215ccd3497", size = 4573714 }, + { url = "https://files.pythonhosted.org/packages/f5/bb/e86e9cf07f73a98d84a4084e8fd420b0e82330a901d9cac8149f994c3417/cryptography-45.0.3-cp311-abi3-win32.whl", hash = "sha256:3ad69eeb92a9de9421e1f6685e85a10fbcfb75c833b42cc9bc2ba9fb00da4710", size = 2934752 }, + { url = "https://files.pythonhosted.org/packages/c7/75/063bc9ddc3d1c73e959054f1fc091b79572e716ef74d6caaa56e945b4af9/cryptography-45.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:97787952246a77d77934d41b62fb1b6f3581d83f71b44796a4158d93b8f5c490", size = 3412465 }, + { url = "https://files.pythonhosted.org/packages/71/9b/04ead6015229a9396890d7654ee35ef630860fb42dc9ff9ec27f72157952/cryptography-45.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:c92519d242703b675ccefd0f0562eb45e74d438e001f8ab52d628e885751fb06", size = 7031892 }, + { url = "https://files.pythonhosted.org/packages/46/c7/c7d05d0e133a09fc677b8a87953815c522697bdf025e5cac13ba419e7240/cryptography-45.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5edcb90da1843df85292ef3a313513766a78fbbb83f584a5a58fb001a5a9d57", size = 4196181 }, + { url = "https://files.pythonhosted.org/packages/08/7a/6ad3aa796b18a683657cef930a986fac0045417e2dc428fd336cfc45ba52/cryptography-45.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38deed72285c7ed699864f964a3f4cf11ab3fb38e8d39cfcd96710cd2b5bb716", size = 4423370 }, + { url = "https://files.pythonhosted.org/packages/4f/58/ec1461bfcb393525f597ac6a10a63938d18775b7803324072974b41a926b/cryptography-45.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5555365a50efe1f486eed6ac7062c33b97ccef409f5970a0b6f205a7cfab59c8", size = 4197839 }, + { url = "https://files.pythonhosted.org/packages/d4/3d/5185b117c32ad4f40846f579369a80e710d6146c2baa8ce09d01612750db/cryptography-45.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e4253ed8f5948a3589b3caee7ad9a5bf218ffd16869c516535325fece163dcc", size = 3886324 }, + { url = "https://files.pythonhosted.org/packages/67/85/caba91a57d291a2ad46e74016d1f83ac294f08128b26e2a81e9b4f2d2555/cryptography-45.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cfd84777b4b6684955ce86156cfb5e08d75e80dc2585e10d69e47f014f0a5342", size = 4450447 }, + { url = "https://files.pythonhosted.org/packages/ae/d1/164e3c9d559133a38279215c712b8ba38e77735d3412f37711b9f8f6f7e0/cryptography-45.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:a2b56de3417fd5f48773ad8e91abaa700b678dc7fe1e0c757e1ae340779acf7b", size = 4200576 }, + { url = "https://files.pythonhosted.org/packages/71/7a/e002d5ce624ed46dfc32abe1deff32190f3ac47ede911789ee936f5a4255/cryptography-45.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:57a6500d459e8035e813bd8b51b671977fb149a8c95ed814989da682314d0782", size = 4450308 }, + { url = "https://files.pythonhosted.org/packages/87/ad/3fbff9c28cf09b0a71e98af57d74f3662dea4a174b12acc493de00ea3f28/cryptography-45.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f22af3c78abfbc7cbcdf2c55d23c3e022e1a462ee2481011d518c7fb9c9f3d65", size = 4325125 }, + { url = "https://files.pythonhosted.org/packages/f5/b4/51417d0cc01802304c1984d76e9592f15e4801abd44ef7ba657060520bf0/cryptography-45.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:232954730c362638544758a8160c4ee1b832dc011d2c41a306ad8f7cccc5bb0b", size = 4560038 }, + { url = "https://files.pythonhosted.org/packages/80/38/d572f6482d45789a7202fb87d052deb7a7b136bf17473ebff33536727a2c/cryptography-45.0.3-cp37-abi3-win32.whl", hash = "sha256:cb6ab89421bc90e0422aca911c69044c2912fc3debb19bb3c1bfe28ee3dff6ab", size = 2924070 }, + { url = "https://files.pythonhosted.org/packages/91/5a/61f39c0ff4443651cc64e626fa97ad3099249152039952be8f344d6b0c86/cryptography-45.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:d54ae41e6bd70ea23707843021c778f151ca258081586f0cfa31d936ae43d1b2", size = 3395005 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674 }, +] + +[[package]] +name = "flask" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "importlib-metadata" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/b7/4ace17e37abd9c21715dea5ee11774a25e404c486a7893fa18e764326ead/flask-2.3.3.tar.gz", hash = "sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc", size = 672756 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/56/26f0be8adc2b4257df20c1c4260ddd0aa396cf8e75d90ab2f7ff99bc34f9/flask-2.3.3-py3-none-any.whl", hash = "sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b", size = 96112 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656 }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344 }, + { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389 }, + { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607 }, + { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728 }, + { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826 }, + { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843 }, + { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219 }, + { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946 }, + { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063 }, + { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506 }, +] + +[[package]] +name = "msal" +version = "1.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/90/81dcc50f0be11a8c4dcbae1a9f761a26e5f905231330a7cacc9f04ec4c61/msal-1.32.3.tar.gz", hash = "sha256:5eea038689c78a5a70ca8ecbe1245458b55a857bd096efb6989c69ba15985d35", size = 151449 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/bf/81516b9aac7fd867709984d08eb4db1d2e3fe1df795c8e442cde9b568962/msal-1.32.3-py3-none-any.whl", hash = "sha256:b2798db57760b1961b142f027ffb7c8169536bf77316e99a0df5c4aaebb11569", size = 115358 }, +] + +[[package]] +name = "msal-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "msal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/99/5d239b6156eddf761a636bded1118414d161bd6b7b37a9335549ed159396/msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4", size = 23315 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583 }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, +] + +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, +] + +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pytest" +version = "8.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/aa/405082ce2749be5398045152251ac69c0f3578c7077efc53431303af97ce/pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6", size = 1515232 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/de/afa024cbe022b1b318a3d224125aa24939e99b4ff6f22e0ba639a2eaee47/pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e", size = 363797 }, +] + +[[package]] +name = "pytest-cov" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841 }, +] + +[[package]] +name = "pytest-env" +version = "1.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "tomli" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/31/27f28431a16b83cab7a636dce59cf397517807d247caa38ee67d65e71ef8/pytest_env-1.1.5.tar.gz", hash = "sha256:91209840aa0e43385073ac464a554ad2947cc2fd663a9debf88d03b01e0cc1cf", size = 8911 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/b8/87cfb16045c9d4092cfcf526135d73b88101aac83bc1adcf82dfb5fd3833/pytest_env-1.1.5-py3-none-any.whl", hash = "sha256:ce90cf8772878515c24b31cd97c7fa1f4481cd68d588419fd45f10ecaee6bc30", size = 6141 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839 }, +] + +[[package]] +name = "urllib3" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, +] + +[[package]] +name = "werkzeug" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498 }, +] + +[[package]] +name = "zipp" +version = "3.22.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/b6/7b3d16792fdf94f146bed92be90b4eb4563569eca91513c8609aebf0c167/zipp-3.22.0.tar.gz", hash = "sha256:dd2f28c3ce4bc67507bfd3781d21b7bb2be31103b51a4553ad7d90b84e57ace5", size = 25257 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/da/f64669af4cae46f17b90798a827519ce3737d31dbafad65d391e49643dc4/zipp-3.22.0-py3-none-any.whl", hash = "sha256:fe208f65f2aca48b81f9e6fd8cf7b8b32c26375266b009b413d45306b6148343", size = 9796 }, +]