diff --git a/.editorconfig b/.editorconfig index b9761641..2c2f31da 100644 --- a/.editorconfig +++ b/.editorconfig @@ -16,6 +16,6 @@ trim_trailing_whitespace = true indent_size = 4 max_line_length = 120 -[*HostFactoryResolver.cs] +[{*HostFactoryResolver.cs,docs/**}] ij_formatter_enabled = false resharper_disable_formatter = true \ No newline at end of file diff --git a/.github/AGENTS.md b/.github/AGENTS.md new file mode 100644 index 00000000..819544d6 --- /dev/null +++ b/.github/AGENTS.md @@ -0,0 +1,54 @@ +# AGENTS.md (GitHub / CI / release) + +## PRs + +- PR titles must use Conventional Commits format. +- Use `.github/pull_request_template.md` for PR descriptions. +- Dependabot PRs are exempt from title validation. + +Allowed types: + +- `feat` +- `fix` +- `docs` +- `refactor` +- `test` +- `chore` +- `ci` + +Allowed scopes: + +- `host` +- `envelopes` +- `abstractions` +- `opentelemetry` +- `source-generators` +- `deps` +- `build` +- `ci` +- `github` +- `core` +- `docs` +- `testing` +- `tests` + +Examples: + +- `feat(host): add handler support` +- `fix(abstractions): resolve dependency issue` +- `docs: update README examples` + +## Releases + +- Release Drafter creates draft releases from PR titles. +- User manually publishes GitHub release. +- Publishing release triggers NuGet package publishing. +- Do not manually bump `Directory.Build.props` for routine releases. +- Do not publish NuGet packages manually. +- Do not create GitHub releases directly. + +Packages version synchronously: + +- `MinimalLambda` +- `MinimalLambda.Abstractions` +- `MinimalLambda.OpenTelemetry` diff --git a/.github/CLAUDE.md b/.github/CLAUDE.md new file mode 120000 index 00000000..47dc3e3d --- /dev/null +++ b/.github/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/.gitignore b/.gitignore index 891a77f4..1754f9ab 100644 --- a/.gitignore +++ b/.gitignore @@ -83,3 +83,4 @@ nunit-*.xml /.sonarqube/ /codecov* **/.venv +/AGENTS.local.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..fa9fe2d3 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,41 @@ +# AGENTS.md (MinimalLambda) + +MinimalLambda is a Lambda-first hosting framework for .NET. +Code under `src/` runs on AWS Lambda or generates code that will. + +## Core guidance + +- Prefer small, focused diffs. +- Match existing patterns; avoid unnecessary refactors. +- Nullable warnings are bugs. +- Prefer AOT/trimming-friendly code. +- Avoid reflection-heavy/dynamic approaches unless required and guarded. +- Run formatting and relevant tests before handoff. + +## Common commands + +```bash +DOTNET_NOLOGO=1 dotnet restore +DOTNET_NOLOGO=1 dotnet tool restore +task format +task test:all +task build:aot-check +``` + +## Repo conventions + +- C# preview is used in many projects. +- C# 14 `extension(...)` blocks are valid. Do not rewrite them to old `this` extension methods. +- XML docs: use only C# XML-supported tags. Do not use unsupported HTML tags like ``. + +## More specific guidance + +- Source/runtime code: `src/AGENTS.md` +- Tests: `tests/AGENTS.md` +- GitHub, PR, release, CI: `.github/AGENTS.md` +- Documentation: `docs/AGENTS.md` + +## Git / PR + +Use `git-workflow` skill for commits, branches, and PRs. +When writing PRs, use `./.github/pull_request_template.md`. diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index db2833ef..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,305 +0,0 @@ -# CLAUDE.md - -## GENERAL - -- Repo link: https://github.com/j-d-ha/minimal-lambda -- GitHub Project Name: MinimalLambda Development -- All code in this project is designed to run on AWS Lambda or generate code that will then be run - on AWS Lambda. -- When writing PRs, ALWAYS use `./.github/pull_request_template.md` as the template for the PR. - -## Code Style - -### C# XML Documentation - -- Only use XML tags that are supported by C#. As an example, do not use ``. - -### Unit Testing - -- **Framework**: xUnit with `[Fact]` and `[Theory]` attributes -- **Mocking**: NSubstitute for creating mocks -- **Test Data**: AutoFixture.Xunit3 with `[AutoNSubstituteData]` for automatic fixture and mock - injection -- **Assertions**: AwesomeAssertions with fluent `.Should()` syntax -- **Keep tests simple and focused**: Only test the class/method being tested, not dependencies -- **Use `[Theory, AutoNSubstituteData]`** for tests that need injected mocks/fixtures -- **Use `[Fact]`** for simple tests with hardcoded values -- **No section comments**: Organize tests logically without `#region` blocks -- **Test only what can be meaningfully tested**: Don't write tests for things that depend entirely - on external library internals - -#### AutoNSubstituteData Pattern - -The `[AutoNSubstituteData]` attribute integrates three testing libraries: - -**How it works:** - -1. **AutoFixture** - Automatically generates test data for any type parameter -2. **NSubstitute** - Automatically replaces all interface types with NSubstitute mocks -3. **xUnit** - Injects these generated instances into test method parameters - -**Example:** - -```csharp -[Theory] -[AutoNSubstituteData] -internal async Task MyTest( - [Frozen] IMyInterface dependency, // Mocked interface (frozen for assertions) - MyClass instanceUnderTest // Auto-constructed with mocked dependencies -) -{ - // Act - await instanceUnderTest.DoSomething(); - - // Assert - verify the frozen mock was called - await dependency.Received(1).ExpectedMethod(); -} -``` - -**Key attributes:** - -- `[Frozen]` - Freezes a mock instance so the same instance is injected into dependent objects. Use - this on parameters you want to assert on later. Without `[Frozen]`, a new mock is created for each - dependency. - -**When to use:** - -- Tests with simple dependencies that don't need custom setup -- Tests where you want to verify a dependency was called (use `[Frozen]` for the dependency) -- Reduces boilerplate compared to manual fixture setup - -**When NOT to use:** - -- Tests needing complex mock behavior configuration (use manual `Fixture` helper class instead) -- Tests requiring specific return values from mocks -- Tests where you need multiple instances of the same type with different configurations - -**Pattern in this codebase:** - -- Prefer `[AutoNSubstituteData]` for simple assertion tests -- Use the manual `Fixture` helper class (in test file) when mocks need `SetupDefaults()` - configuration -- Combine both: use `[AutoNSubstituteData]` with `[Frozen]` to inject configured mocks from a manual - fixture - -# Test Commands - -Task wrappers (recommended): - -```bash -task test:all # dotnet test (Release) -task test:verbose # dotnet test (detailed logs) -task test:coverage # dotnet test + coverage -task test:watch # dotnet watch ... test -``` - -Direct `dotnet` commands: - -```bash -# Run the full test suite (solution-level) -DOTNET_NOLOGO=1 dotnet test --configuration Release - -# Faster inner loop: run a single target framework (projects are multi-targeted) -DOTNET_NOLOGO=1 dotnet test --configuration Release -f net10.0 - -# Run a single test project -DOTNET_NOLOGO=1 dotnet test \ - --project tests/MinimalLambda.UnitTests/MinimalLambda.UnitTests.csproj \ - --configuration Release -f net10.0 - -# Verbose output for debugging -DOTNET_NOLOGO=1 dotnet test --configuration Release --logger "console;verbosity=detailed" -``` - -Run a single test (xUnit v3 + Microsoft.Testing.Platform) - -This repo pins the test runner in `global.json`: - -```json -"test": {"runner": "Microsoft.Testing.Platform"} -``` - -```bash -# list tests (copy fully-qualified name) -DOTNET_NOLOGO=1 dotnet test \ - --project tests/MinimalLambda.UnitTests/MinimalLambda.UnitTests.csproj \ - -f net10.0 -v q \ - --list-tests --no-progress --no-ansi - -# run one test method -DOTNET_NOLOGO=1 dotnet test \ - --project tests/MinimalLambda.UnitTests/MinimalLambda.UnitTests.csproj \ - -f net10.0 -v q \ - --filter-method "MyNamespace.MyTestClass.MyTestMethod" \ - --minimum-expected-tests 1 \ - --no-progress --no-ansi - -# handy filters -DOTNET_NOLOGO=1 dotnet test --project tests/MinimalLambda.UnitTests/MinimalLambda.UnitTests.csproj -f net10.0 -v q \ - --filter-class "MyNamespace.MyTestClass" --minimum-expected-tests 1 --no-progress --no-ansi -DOTNET_NOLOGO=1 dotnet test --project tests/MinimalLambda.UnitTests/MinimalLambda.UnitTests.csproj -f net10.0 -v q \ - --filter-namespace "MyNamespace.Tests" --minimum-expected-tests 1 --no-progress --no-ansi -``` - -# C# 14 Extension Members - Valid Syntax - -## This is VALID C# 14 syntax - do NOT change it - -C# 14 introduces **extension blocks** using the `extension` keyword. This is NEW, CORRECT syntax. - -## Valid Extension Block Syntax - -```csharp -public static class MyExtensions -{ - extension(string str) - { - public int WordCount() => str.Split().Length; - - public string FirstWord => str.Split().FirstOrDefault() ?? ""; - } -} -``` - -This is equivalent to the old syntax but supports properties and operators too: - -```csharp -// Old syntax (still valid) -public static class MyExtensions -{ - public static int WordCount(this string str) => str.Split().Length; -} -``` - -## Key Rules - -1. Extension blocks go inside **static classes** -2. Use `extension(Type receiver)` syntax -3. Members inside access `receiver` directly (no `this.`) -4. Supports: methods, properties, operators -5. Can use `extension(ref Type receiver)` for value types - -## For Ref Extensions - -```csharp -extension(ref int number) -{ - public void Increment() => number++; -} -``` - -## Do NOT Convert - -- ✅ `extension(string str)` is CORRECT -- ❌ Do NOT change to `this string str` -- ❌ Do NOT suggest "fixing" this syntax -- ❌ Do NOT flag as an error - -This is modern C# 14. Leave it as-is. - -## Release & Versioning Workflow - -### Commit Message Requirements - -When Claude makes commits, ALWAYS follow conventional commits format: - -``` -(scope): - -[optional body] - -[optional footer(s)] -``` - -**Types:** `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `ci` - -**Scope (optional):** If used, it must be one of these exact values: - -- `host` -- `envelopes` -- `abstractions` -- `opentelemetry` -- `source-generators` -- `deps` -- `build` -- `ci` -- `github` -- `core` -- `docs` -- `testing` -- `tests` - -- Omit scope for general or mixed changes when no single valid scope fits - -**Examples:** - -- `feat(host): add new Lambda handler support` -- `fix(abstractions): resolve dependency issue` -- `docs: update README with examples` -- `chore(deps): bump dependency version` - -**Breaking Changes:** Include `BREAKING CHANGE:` in footer: - -``` -feat(host): redesign handler pipeline - -BREAKING CHANGE: Handler API has changed -``` - -### Pull Request Title Requirements - -When creating PRs, the title MUST follow conventional commits format (same rules as commit -messages): - -- Strict validation is enforced by CI -- Format: `(scope): ` or `: ` -- If a scope is present, it must be one of the valid scopes listed above -- Dependabot PRs are exempt from this requirement - -**Examples:** - -- `feat(host): add new Lambda handler support` -- `fix(abstractions): resolve dependency issue` -- `docs: update README with examples` - -### Versioning - -All 3 packages are versioned synchronously: - -- `MinimalLambda` -- `MinimalLambda.Abstractions` -- `MinimalLambda.OpenTelemetry` - -Versions are stored in `/Directory.Build.props` as ``. - -**Version bumping (automatic):** - -- `fix:` commits → patch version bump (e.g., 1.0.0 → 1.0.1) -- `feat:` commits → minor version bump (e.g., 1.0.0 → 1.1.0) -- `BREAKING CHANGE` footer → major version bump (e.g., 1.0.0 → 2.0.0) - -### Release Process - -1. **Draft Release:** Release Drafter automatically creates a draft release on each push to main, - organizing changes by type -2. **Manual Release:** User manually publishes the draft release from GitHub -3. **Automated Publishing:** Publishing a release triggers automatic NuGet package publishing to - nuget.org -4. **Pre-release Designation:** Manual - user designates whether a release is pre-release ( - alpha/beta) or stable - -### Release Notes & Changelog - -**Release Drafter** automatically creates draft releases with organized changelog from PR titles. - -When a release is published, the GitHub release description is updated with the changelog. - -### Claude's Role in Release Workflow - -- **DO** create commits following conventional commits format -- **DO** title PRs following conventional commits format -- **DO** reference the PR/commit scope when relevant -- **DO** include breaking change footers for incompatible changes -- **DO NOT** manually bump versions in Directory.Build.props (automatic) -- **DO NOT** publish to NuGet manually (automated on release) -- **DO NOT** create GitHub releases directly (use Release Drafter) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 00000000..47dc3e3d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index 50f276fb..bcf38798 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,27 +5,27 @@ - - - - - - - + + + + + + + - - - + + + - - + + @@ -37,31 +37,31 @@ - - - - + + + + - + - + - + - - - - + + + + @@ -79,9 +79,9 @@ - - - + + + diff --git a/MinimalLambda.sln b/MinimalLambda.sln index 1143161b..40bbb2ff 100644 --- a/MinimalLambda.sln +++ b/MinimalLambda.sln @@ -15,8 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution LICENSE = LICENSE .gitignore = .gitignore Directory.Build.props = Directory.Build.props - CLAUDE.md = CLAUDE.md - CLAUDE.local.md = CLAUDE.local.md + AGENTS.md = AGENTS.md + AGENTS.local.md = AGENTS.local.md Directory.Packages.props = Directory.Packages.props THIRD-PARTY-LICENSES.txt = THIRD-PARTY-LICENSES.txt mkdocs.yml = mkdocs.yml diff --git a/docs/AGENTS.md b/docs/AGENTS.md new file mode 100644 index 00000000..6a1b55d4 --- /dev/null +++ b/docs/AGENTS.md @@ -0,0 +1,9 @@ +# AGENTS.md (MinimalLambda docs) + +Docs are MkDocs markdown content. + +- Keep examples accurate and buildable. +- Prefer concise, task-oriented guidance. +- Use repo terminology consistently: Lambda-first, AOT-friendly, handler, envelope, middleware. +- Update navigation/config when adding or moving pages. +- Keep code snippets aligned with current public APIs. diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md new file mode 120000 index 00000000..47dc3e3d --- /dev/null +++ b/docs/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/docs/features/envelopes.md b/docs/features/envelopes.md index 31ae1887..1eccf804 100644 --- a/docs/features/envelopes.md +++ b/docs/features/envelopes.md @@ -13,19 +13,19 @@ serialization for both built-in envelopes and your own custom envelope types. ## Why Envelopes? - **Strong typing** – `SqsEnvelope` ensures handlers only run when payloads deserialize into - `Foo`. + `Foo`. - **Zero boilerplate** – No more `JsonSerializer.Deserialize` calls sprinkled through handlers. - **Consistent serialization** – `EnvelopeOptions` applies globally, including Native AOT - `JsonSerializerContext` support. + `JsonSerializerContext` support. - **Extensible** – Implement `IRequestEnvelope`/`IResponseEnvelope` for proprietary event shapes or - alternative serialization formats (XML, Protobuf, etc.). + alternative serialization formats (XML, Protobuf, etc.). ## Provided Envelopes Install only the envelopes you need; each one lives in its own NuGet package. -| Event Source | Package | NuGet | -|---------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Event Source | Package | NuGet | +| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Infrastructure / Base | [MinimalLambda.Envelopes](https://github.com/j-d-ha/minimal-lambda/tree/main/src/Envelopes/MinimalLambda.Envelopes) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes) | | SQS | [MinimalLambda.Envelopes.Sqs](https://github.com/j-d-ha/minimal-lambda/tree/main/src/Envelopes/MinimalLambda.Envelopes.Sqs) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs) | | SNS | [MinimalLambda.Envelopes.Sns](https://github.com/j-d-ha/minimal-lambda/tree/main/src/Envelopes/MinimalLambda.Envelopes.Sns) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns) | @@ -37,6 +37,7 @@ Install only the envelopes you need; each one lives in its own NuGet package. | Application Load Balancer (ALB) | [MinimalLambda.Envelopes.Alb](https://github.com/j-d-ha/minimal-lambda/tree/main/src/Envelopes/MinimalLambda.Envelopes.Alb) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Alb.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Alb) | !!! note "Infrastructure Package" + `MinimalLambda.Envelopes` is automatically referenced by ALB and API Gateway packages. It provides `IHttpResult` and extension methods for the response builder API. You don't need to install it directly. @@ -110,15 +111,14 @@ internal record LoginSuccess(string Token, DateTime ExpiresAt); ### Available Result Classes -| Class | Package | Use Case | -|------------------------|--------------------------------------|---------------------------------------------| -| `AlbResult` | MinimalLambda.Envelopes.Alb | Application Load Balancer responses | -| `ApiGatewayResult` | MinimalLambda.Envelopes.ApiGateway | REST API / HTTP API v1 / WebSocket | -| `ApiGatewayV2Result` | MinimalLambda.Envelopes.ApiGateway | HTTP API v2 responses | +| Class | Package | Use Case | +| -------------------- | ---------------------------------- | ----------------------------------- | +| `AlbResult` | MinimalLambda.Envelopes.Alb | Application Load Balancer responses | +| `ApiGatewayResult` | MinimalLambda.Envelopes.ApiGateway | REST API / HTTP API v1 / WebSocket | +| `ApiGatewayV2Result` | MinimalLambda.Envelopes.ApiGateway | HTTP API v2 responses | Common methods: `Ok()`, `Created()`, `NoContent()`, `BadRequest()`, `Unauthorized()`, `NotFound()`, -`Conflict()`, `UnprocessableEntity()`, `InternalServerError()`, `StatusCode(int)`, `Text(int, -string)`, `Json(int, T)`. All methods have overloads with and without body content. +`Conflict()`, `UnprocessableEntity()`, `InternalServerError()`, `StatusCode(int)`, `Text(int, string)`, `Json(int, T)`. All methods have overloads with and without body content. ### When to Use Results vs. Envelopes @@ -129,6 +129,7 @@ Provides convenient methods for common HTTP status codes. envelope base classes for custom behavior. !!! tip "Complete API Reference" + For detailed method documentation, AOT configuration, and advanced usage, see the package README files: @@ -136,6 +137,7 @@ envelope base classes for custom behavior. - [API Gateway Package README](https://github.com/j-d-ha/minimal-lambda/tree/main/src/Envelopes/MinimalLambda.Envelopes.ApiGateway) !!! note + Result classes use their respective envelope classes internally (`AlbResponseEnvelope`, `ApiGatewayResponseEnvelope`, etc.). They're a convenience layer over the envelope infrastructure. @@ -145,6 +147,7 @@ envelope base classes for custom behavior. When using .NET Native AOT, register all envelope and payload types in your `JsonSerializerContext`. !!! tip "Register Both Envelope and Payload Types" + You must register **both** the envelope type (e.g., `ApiGatewayRequestEnvelope`) **and** the inner payload type (e.g., `LoginRequest`). The envelope wraps the AWS event structure, while the payload is your business type inside the envelope. @@ -194,6 +197,7 @@ builder.Services.ConfigureEnvelopeOptions(options => ``` !!! important "Why Register in Two Places?" + The context must be registered as the type resolver for **both** the envelope options and the Lambda serializer because deserialization happens at different steps: @@ -228,9 +232,10 @@ envelope using `System.Xml`. See the SQS README in the repo for a complete XML s ### Advanced Configuration - **`LambdaDefaultJsonOptions`** – minimal-lambda maintains a second `JsonSerializerOptions` - instance for Lambda-specific envelopes (e.g., SNS→SQS fan-out). Most apps shouldn’t touch it; the - host copies your `JsonOptions.TypeInfoResolver` automatically. Only override it when you need - different converters for those hybrid envelopes. + instance for Lambda-specific envelopes (e.g., SNS→SQS fan-out). Most apps shouldn’t touch it; the + host copies your `JsonOptions.TypeInfoResolver` automatically. Only override it when you need + different converters for those hybrid envelopes. + - **`Items` dictionary** – Store arbitrary context for custom envelopes: ```csharp title="Program.cs" linenums="1" @@ -294,15 +299,15 @@ typed object. ## Best Practices - **Check for null** – Always guard against `BodyContent`/`PayloadContent` being `null`. Set it to - `null` if deserialization fails instead of throwing. + `null` if deserialization fails instead of throwing. - **Use `[JsonIgnore]`** – Keep serialized strings (`Body`, `Payload`, etc.) separate from the - deserialized object to avoid recursive serialization. + deserialized object to avoid recursive serialization. - **Return `SQSBatchResponse` when required** – For SQS/SNS to SQS fan-out scenarios, populate - `BatchItemFailures` to signal per-message errors. + `BatchItemFailures` to signal per-message errors. - **Centralize configuration** – Prefer `ConfigureEnvelopeOptions` or configuration binding over - ad-hoc serializer tweaks. + ad-hoc serializer tweaks. - **Log deserialization issues** – Logging helps diagnose malformed payloads without crashing the - Lambda. + Lambda. ## When to Use Envelopes diff --git a/docs/features/index.md b/docs/features/index.md index 43fc44bf..236163a3 100644 --- a/docs/features/index.md +++ b/docs/features/index.md @@ -4,7 +4,7 @@ The `MinimalLambda` framework provides a rich ecosystem of features and extensio This section provides an overview of the available features. ---- +______________________________________________________________________ ## Feature Categories @@ -15,4 +15,3 @@ The Envelope pattern provides type-safe wrappers for various AWS event sources l ### [Observability (OpenTelemetry)](./open_telemetry.md) This feature provides comprehensive observability through OpenTelemetry integration. It enables distributed tracing and metrics collection, offering deep insights into your Lambda function's performance and behavior. - diff --git a/docs/features/open_telemetry.md b/docs/features/open_telemetry.md index 64bbbac7..edf8a19d 100644 --- a/docs/features/open_telemetry.md +++ b/docs/features/open_telemetry.md @@ -6,7 +6,7 @@ The `MinimalLambda.OpenTelemetry` package provides seamless integration with the At compile time, it wraps your handler invocation in a root trace span, providing reflection-free, high-performance distributed tracing. ---- +______________________________________________________________________ ## Key Benefits @@ -17,7 +17,7 @@ At compile time, it wraps your handler invocation in a root trace span, providin - **Standard APIs**: Leverages the standard OpenTelemetry .NET SDK, so you can use familiar configuration and custom instrumentation APIs. - **Custom Instrumentation**: Easily create custom spans (`Activity`) and metrics (`Meter`) to capture application-specific logic. ---- +______________________________________________________________________ ## Quick Start @@ -104,11 +104,12 @@ internal record Response(string Message); 9. Write your Lambda handler like normal. !!! tip - OpenTelemetry tracing can be configured in multiple ways, including manually creating a trace provider using the [OpenTelemetry](https://www.nuget.org/packages/OpenTelemetry), or through registering OpenTelemetry services in your DI container using [OpenTelemetry.Extensions.Hosting](https://www.nuget.org/packages/OpenTelemetry.Extensions.Hosting). - When working with `MinimalLambda`, its recommended to the latter approach and as such, this documentation focuses on it. + OpenTelemetry tracing can be configured in multiple ways, including manually creating a trace provider using the [OpenTelemetry](https://www.nuget.org/packages/OpenTelemetry), or through registering OpenTelemetry services in your DI container using [OpenTelemetry.Extensions.Hosting](https://www.nuget.org/packages/OpenTelemetry.Extensions.Hosting). ---- + When working with `MinimalLambda`, its recommended to the latter approach and as such, this documentation focuses on it. + +______________________________________________________________________ ## How It Works: Feature-Based Middleware @@ -119,14 +120,14 @@ Here's the step-by-step process: 1. The `UseOpenTelemetryTracing()` middleware is registered in the invocation pipeline when you call the method on your Lambda application. 2. During each Lambda invocation, the middleware executes before your handler runs. 3. The middleware dynamically detects the event and response types by reading from the feature collection: - - It checks for an `IEventFeature` to get the incoming event object - - It checks for an `IResponseFeature` to get the outgoing response object + - It checks for an `IEventFeature` to get the incoming event object + - It checks for an `IResponseFeature` to get the outgoing response object 4. These event and response objects are passed to the official `OpenTelemetry.Instrumentation.AWSLambda` package, which wraps the handler execution in a root trace span. 5. The OpenTelemetry instrumentation automatically extracts trace context from supported event types (like API Gateway requests), ensuring proper distributed trace propagation across services. The shutdown helpers (`OnShutdownFlushOpenTelemetry`, `OnShutdownFlushTracer`, and `OnShutdownFlushMeter`) are regular extension methods that execute at runtime and use the registered `TracerProvider`/`MeterProvider` instances to force-flush telemetry before Lambda freezes the environment. ---- +______________________________________________________________________ ## Working With `MinimalLambda.OpenTelemetry` @@ -157,7 +158,6 @@ This method registers middleware in the invocation pipeline that wraps each hand Because the middleware reads event and response types dynamically from the feature collection, it works seamlessly with any Lambda event source type. The OpenTelemetry instrumentation can correctly extract context and attributes from strongly-typed event objects (such as trace parent headers from an API Gateway request), ensuring proper distributed trace propagation across your services. - ### Gracefully Shutdown & Cleaning Up The OpenTelemetry `TracerProvider` and `MeterProvider` services both implement `IDisposable`. When the dependency injection container is disposed of during a normal application shutdown, it should trigger these providers to automatically flush any buffered telemetry. However, in a serverless environment where the lifecycle can be abrupt, this disposal is not always guaranteed to complete before the execution environment is frozen. @@ -167,7 +167,7 @@ For situations where you notice data being dropped, or if you want to guarantee The following methods are available to be called on the `LambdaApplication` instance: | Method | Description | -|----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `OnShutdownFlushOpenTelemetry()` | A convenience method that registers shutdown hooks to flush both traces and metrics. It calls both `OnShutdownFlushTracer` and `OnShutdownFlushMeter` internally. This is the recommended method for most users. | | `OnShutdownFlushTracer()` | Registers a shutdown hook to force-flush only the `TracerProvider`. Use this if you are only tracing and not collecting metrics, or if you need separate control over flushing traces. | | `OnShutdownFlushMeter()` | Registers a shutdown hook to force-flush only the `MeterProvider`. Use this if you are only collecting metrics and not tracing. | @@ -175,20 +175,23 @@ The following methods are available to be called on the `LambdaApplication` inst For most applications, calling `lambda.OnShutdownFlushOpenTelemetry()` is sufficient to ensure all telemetry is flushed. If your application only uses tracing or metrics, but not both, you can use the more specific methods for clarity. !!! note + These methods call `GetRequiredService()` and `GetRequiredService()`. Make sure those providers are registered (via `.AddOpenTelemetry().WithTracing(...)` / `.WithMetrics(...)`) before invoking the shutdown helpers, otherwise the application will throw during startup. All three methods also accept an optional `timeoutMilliseconds` parameter. This allows you to specify a maximum duration for the flush operation. Importantly, these flush operations are non-blocking and respect the provided `CancellationToken`, ensuring they can gracefully exit if the Lambda execution environment signals a shutdown before the timeout elapses. This combined approach offers robust control over the flush duration within the limited time available during a Lambda shutdown. !!! Warning + When shutting down, Lambda only allocates up to 500ms of time for the execution environment to shut down. As such, it is important to make sure that shutdown logic such as flushing traces is executed as quickly as possible. More information about the Lambda execution environment lifecycle can be found [here](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtime-environment.html). ---- +______________________________________________________________________ ## Manual Instrumentation `MinimalLambda.OpenTelemetry` helps you instrement your Lambda handlers with [OpenTelemetry.Instrumentation.AWSLambda](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.AWSLambda), but to get the most out of observability, you should add custom instrumentation to your application code. In this section we cover how this can be done easily with the Dependancy Injection support provided by `MinimalLambda`. !!! note + This code is not specific to `MinimalLambda.OpenTelemetry` and follows the guidlines provided by Microsoft's [.NET distributed tracing documetation](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing). A full working example of an instrumented Lambda application can be found [on GitHub](https://github.com/j-d-ha/minimal-lambda/tree/main/examples/MinimalLambda.Example.OpenTelemetry). diff --git a/docs/getting-started/core-concepts.md b/docs/getting-started/core-concepts.md index 807c621b..4d5f5bbb 100644 --- a/docs/getting-started/core-concepts.md +++ b/docs/getting-started/core-concepts.md @@ -117,11 +117,11 @@ graph LR `MinimalLambda` uses the standard `Microsoft.Extensions.DependencyInjection` container. Registrations happen before `builder.Build()`, and the framework aligns service lifetimes with the Lambda lifecycle. -| Lifetime | Created | Disposed | Use for | -|-----------|--------------------------------------|-------------------------------------------|---------------------------------------------| -| Singleton | During OnInit (cold start) | When AWS tears down the execution context | HttpClient, caches, configuration, SDKs | -| Scoped | At the beginning of each invocation | After the invocation completes | DbContext, repositories, per-request state | -| Transient | Each time the service is requested | With the consuming scope | Lightweight helpers, formatters, pure logic | +| Lifetime | Created | Disposed | Use for | +| --------- | ----------------------------------- | ----------------------------------------- | ------------------------------------------- | +| Singleton | During OnInit (cold start) | When AWS tears down the execution context | HttpClient, caches, configuration, SDKs | +| Scoped | At the beginning of each invocation | After the invocation completes | DbContext, repositories, per-request state | +| Transient | Each time the service is requested | With the consuming scope | Lightweight helpers, formatters, pure logic | **Tips:** diff --git a/docs/getting-started/first-lambda.md b/docs/getting-started/first-lambda.md index 68f058ac..e20af669 100644 --- a/docs/getting-started/first-lambda.md +++ b/docs/getting-started/first-lambda.md @@ -26,6 +26,7 @@ Before starting, ensure you've completed the [Installation](installation.md) gui Create strongly-typed models for your Lambda's input and output using C# records. !!! note "Keep supporting types at the bottom" + Place records, interfaces, and service implementations **after** the main pipeline (after `await lambda.RunAsync();`). This keeps startup logic together at the top while still shipping everything as a single file. @@ -49,9 +50,11 @@ public record GreetingResponse( ``` !!! tip "Why Records?" + Records provide immutable data models with built-in equality and deconstruction. They're perfect for Lambda events and responses. !!! note "JsonPropertyName Attribute" + The `JsonPropertyName` attribute ensures your JSON property names follow your preferred casing convention (e.g., camelCase for JavaScript clients). ## Step 2: Create a Service Interface @@ -114,6 +117,7 @@ var lambda = builder.Build(); ``` !!! info "Service Lifetime: Singleton" + We use `AddSingleton` because our `GreetingService` is stateless and can be shared across all invocations. This is more efficient than creating a new instance for each request. ## Step 5: Add Middleware (Optional) @@ -135,9 +139,11 @@ lambda.UseMiddleware( ``` !!! tip "Middleware Use Cases" + Middleware is perfect for cross-cutting concerns like logging, metrics, validation, error handling, and authentication. !!! note "Clear Lambda Output Formatting" + The .NET Lambda runtime captures standard output/error and re-wraps every line into its own structured log record. If you're running locally or relying on a custom logger (Serilog, NLog, etc.), disable that extra formatting so your log payloads stay untouched: @@ -168,7 +174,8 @@ lambda.MapHandler( ); ``` -!!! warning "The [FromEvent] Attribute" +!!! warning "The `[FromEvent]` Attribute" + Add `[FromEvent]` to at most one handler parameter to mark it as the deserialized Lambda event. If your handler does not accept an event payload (e.g., scheduled invocations or DI-only inputs), you can omit the attribute entirely. ## Step 7: Run the Lambda @@ -569,7 +576,7 @@ sequenceDiagram **Solution**: Verify you've added `[property: JsonPropertyName("...")]` attributes to your record properties. -### Build Errors with [FromEvent] Attribute +### Build Errors with `[FromEvent]` Attribute **Error**: `The [FromEvent] attribute is not recognized` @@ -580,6 +587,7 @@ sequenceDiagram **Error**: Lambda times out during execution **Solution**: + - Increase timeout in your deployment template (default is 30 seconds) - Check for blocking operations in your handler - Ensure async/await is used correctly diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index ec32ab13..e8b75690 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -19,6 +19,7 @@ Before you begin, ensure you have: - **AWS Account** – For deploying and testing (optional for local development) !!! tip "IDE Recommendations" + - Visual Studio 2022 (17.8+) - JetBrains Rider 2023.3+ - Visual Studio Code with C# Dev Kit @@ -29,7 +30,7 @@ Before you begin, ensure you have: - **[Your First Lambda](first-lambda.md)** – Walk through a handler, DI setup, and local testing. - **[Core Concepts](core-concepts.md)** – Learn about the host lifecycle, middleware, and source generation. - **Testing** – For in-memory, `WebApplicationFactory`-style integration tests, see - [MinimalLambda.Testing](../guides/testing.md). + [MinimalLambda.Testing](../guides/testing.md). Prefer to explore? Head directly to **[Guides](../guides/index.md)** or **[Examples](../examples/index.md)** for deeper dives. diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 3d873889..e39d9c37 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -7,13 +7,14 @@ This guide walks you through installing `MinimalLambda` and configuring your pro Before you begin, ensure your development environment meets these requirements: | Requirement | Minimum Version | Recommended | -|---------------------|-------------------------------------------------------|-------------| +| ------------------- | ----------------------------------------------------- | ----------- | | .NET SDK | 8.0 | Latest LTS | | C# Language Version | 11 | latest | | IDE | Visual Studio 2022 (17.8+), Rider 2023.3+, or VS Code | Latest | | AWS CLI | 2.0+ (optional) | Latest | !!! note "C# 11 Requirement" + C# 11 or later is required for source generators and interceptors that power the framework's compile-time optimizations. ## Installing the NuGet Package @@ -124,6 +125,7 @@ Here's a complete, minimal `.csproj` file for a Lambda function: ``` !!! info "Source Generators" + The `MinimalLambda` package ships an MSBuild target that automatically registers the required interceptor namespaces. No additional configuration is needed in your project file. ## Verifying Installation @@ -170,6 +172,7 @@ Build succeeded. ``` !!! success "Installation Successful" + If the build succeeds, your installation is complete and you're ready to build Lambda functions! ## Package Overview @@ -178,8 +181,8 @@ The `MinimalLambda` framework includes multiple packages for different use cases ### Core Packages -| Package | Purpose | When to Use | -|----------------------------------|---------------------------|-----------------------------------------------| +| Package | Purpose | When to Use | +| ------------------------------- | ------------------------- | --------------------------------------------- | | **MinimalLambda** | Core framework | Required for all Lambda functions | | **MinimalLambda.Abstractions** | Interfaces and contracts | When creating custom extensions or middleware | | **MinimalLambda.OpenTelemetry** | Observability integration | When you need distributed tracing and metrics | @@ -188,19 +191,20 @@ The `MinimalLambda` framework includes multiple packages for different use cases Envelope packages provide type-safe, strongly-typed event handling for specific AWS event sources: -| Package | Event Source | When to Use | -|----------------------------------------------|---------------------------|-------------------------------| +| Package | Event Source | When to Use | +| ------------------------------------------- | ------------------------- | ---------------------------------------- | | **MinimalLambda.Envelopes** | Infrastructure | HTTP response builders (auto-referenced) | -| **MinimalLambda.Envelopes.Sqs** | Amazon SQS | Processing SQS queue messages | -| **MinimalLambda.Envelopes.Sns** | Amazon SNS | Handling SNS notifications | -| **MinimalLambda.Envelopes.ApiGateway** | API Gateway | Building REST/HTTP APIs | -| **MinimalLambda.Envelopes.Kinesis** | Kinesis Data Streams | Processing stream records | -| **MinimalLambda.Envelopes.KinesisFirehose** | Kinesis Firehose | Transforming Firehose data | -| **MinimalLambda.Envelopes.Kafka** | Apache Kafka / MSK | Processing Kafka messages | -| **MinimalLambda.Envelopes.CloudWatchLogs** | CloudWatch Logs | Processing log subscriptions | -| **MinimalLambda.Envelopes.Alb** | Application Load Balancer | ALB target Lambda functions | +| **MinimalLambda.Envelopes.Sqs** | Amazon SQS | Processing SQS queue messages | +| **MinimalLambda.Envelopes.Sns** | Amazon SNS | Handling SNS notifications | +| **MinimalLambda.Envelopes.ApiGateway** | API Gateway | Building REST/HTTP APIs | +| **MinimalLambda.Envelopes.Kinesis** | Kinesis Data Streams | Processing stream records | +| **MinimalLambda.Envelopes.KinesisFirehose** | Kinesis Firehose | Transforming Firehose data | +| **MinimalLambda.Envelopes.Kafka** | Apache Kafka / MSK | Processing Kafka messages | +| **MinimalLambda.Envelopes.CloudWatchLogs** | CloudWatch Logs | Processing log subscriptions | +| **MinimalLambda.Envelopes.Alb** | Application Load Balancer | ALB target Lambda functions | !!! info "Envelope Packages" + You only need envelope packages if you're working with those specific event sources. For simple use cases, just `MinimalLambda` is sufficient. Learn more in the [Envelopes documentation](../features/envelopes.md). ## Troubleshooting @@ -218,6 +222,7 @@ Envelope packages provide type-safe, strongly-typed event handling for specific **Error**: Various build errors after adding the package **Solution**: + 1. Verify your .NET SDK version: `dotnet --version` 2. Ensure it's .NET 8.0 or later 3. Clean and rebuild: `dotnet clean && dotnet build` diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md index 7a1db5be..47e9726f 100644 --- a/docs/guides/configuration.md +++ b/docs/guides/configuration.md @@ -6,11 +6,13 @@ objects. On top of that, `LambdaHostOptions` control the Lambda-specific runtime shutdown windows, serializer choices, etc.). This guide covers both layers. !!! warning "Breaking Change - Configuration Section Renamed" + Starting with version 2.0.0, the configuration section name has changed from `AwsLambdaHost` to `LambdaHost`. **Migration Required:** Update your `appsettings.json`: + ```json // Before { @@ -28,6 +30,7 @@ shutdown windows, serializer choices, etc.). This guide covers both layers. ``` Update environment variables: + - `AwsLambdaHost__InvocationCancellationBuffer` → `LambdaHost__InvocationCancellationBuffer` - Pattern: `AwsLambdaHost__*` → `LambdaHost__*` @@ -45,6 +48,7 @@ providers are added in the following order (later entries override earlier ones) 6. All remaining environment variables (no prefix filter). !!! warning "`ASPNETCORE_` Prefixed Environment Variable" + Enviroment variables with the `ASPNETCORE_` prefix are not automatically loaded by `CreateBuilder()` and must be added manually. As such, the enviroment cannot be set using the `ASPNETCORE_ENVIRONMENT` environment variable. To load environment variables prefixed with `ASPNETCORE_` after calling `CreateBuilder()`, you can add the following code: @@ -53,13 +57,13 @@ providers are added in the following order (later entries override earlier ones) var builder = LambdaApplication.CreateBuilder(); builder.Configuration.AddEnvironmentVariables("ASPNETCORE_"); ``` - + If you need to use `ASPNETCORE_` prefixed environment variables when creating the `LambdaApplicationBuilder`, you can add the following code: ```csharp var configuration = new ConfigurationManager(); configuration.AddEnvironmentVariables("ASPNETCORE_"); - + var builder = LambdaApplication.CreateBuilder( new LambdaApplicationOptions { Configuration = configuration } ); @@ -106,7 +110,7 @@ builder.Services.ConfigureLambdaHostOptions(options => ``` | Option | Type | Default | Description | -|--------------------------------|--------------------------|-----------------------------------------------|------------------------------------------------------------------------------------| +| ------------------------------ | ------------------------ | --------------------------------------------- | ---------------------------------------------------------------------------------- | | `InitTimeout` | `TimeSpan` | 5 seconds | Maximum time all `OnInit` handlers collectively have before cancellation. | | `InvocationCancellationBuffer` | `TimeSpan` | 500 milliseconds | Buffer subtracted from remaining time before the invocation token fires. | | `ShutdownDuration` | `TimeSpan` | `ShutdownDuration.ExternalExtensions` (500ms) | Expected window between SIGTERM and SIGKILL. | @@ -291,11 +295,11 @@ public class OrderService : IOrderService } ``` -| Interface | Lifetime | Reloads | Lambda Guidance | -|-----------------------|-----------|----------------|--------------------------------------------------------------------| -| `IOptions` | Singleton | Never | ✅ Recommended—configuration ships with the deployment package. | -| `IOptionsSnapshot` | Scoped | Per invocation | Use only if you vary config between invocations (rare). | -| `IOptionsMonitor` | Singleton | On change | Rarely useful unless you reload from remote providers at runtime. | +| Interface | Lifetime | Reloads | Lambda Guidance | +| --------------------- | --------- | -------------- | ----------------------------------------------------------------- | +| `IOptions` | Singleton | Never | ✅ Recommended—configuration ships with the deployment package. | +| `IOptionsSnapshot` | Scoped | Per invocation | Use only if you vary config between invocations (rare). | +| `IOptionsMonitor` | Singleton | On change | Rarely useful unless you reload from remote providers at runtime. | ### Environment-Specific Files @@ -398,7 +402,7 @@ For more complex rules implement `IValidatableObject` or add a custom validator. - **Copy `appsettings.*` to the output** – Without it, Lambda cannot load the files. - **Use environment variables for secrets** – Combine SAM/CDK parameters with Secrets Manager references. - **Stick to `LambdaHost` section for framework knobs** – Keeps host settings discoverable and - separate from business configuration. + separate from business configuration. - **Clear Lambda output formatting when you own logging** – Avoid double-wrapping JSON payloads. ## Troubleshooting diff --git a/docs/guides/dependency-injection.md b/docs/guides/dependency-injection.md index f064572b..14dd50cd 100644 --- a/docs/guides/dependency-injection.md +++ b/docs/guides/dependency-injection.md @@ -21,14 +21,14 @@ var lambda = builder.Build(); - `builder.Services` is the same `IServiceCollection` you use everywhere else in .NET. - All registrations must happen **before** `builder.Build()`. - Keep supporting types (records, services, options classes) at the bottom of `Program.cs`; keep the - pipeline (DI, middleware, handlers, run) at the top so cold-start work stays easy to read. + pipeline (DI, middleware, handlers, run) at the top so cold-start work stays easy to read. ## Service Lifetimes in Lambda Lambda containers live across multiple invocations. Map the standard lifetimes to Lambda's lifecycle: | Lifetime | When it's created | When it's disposed | Use for | -|-----------|----------------------------------|-------------------------------------------|--------------------------------------------| +| --------- | -------------------------------- | ----------------------------------------- | ------------------------------------------ | | Singleton | During OnInit (first cold start) | When the execution environment shuts down | HttpClient, caches, telemetry, config | | Scoped | Once per invocation | After the invocation completes | DbContext, repositories, per-request state | | Transient | Every time it's requested | After the requesting scope is disposed | Lightweight helpers, pure functions | @@ -37,7 +37,7 @@ Lambda containers live across multiple invocations. Map the standard lifetimes t - Scoped services are the default choice for anything that shouldn't leak state between invocations. - Transients work the same as in ASP.NET Core, but prefer Scoped unless you truly need a new instance - every time a constructor runs. + every time a constructor runs. ## Invocation Scope and `ILambdaInvocationContext` @@ -68,19 +68,20 @@ lambda.MapHandler(async ( If your handler doesn't need the Lambda payload, omit the `[FromEvent]` parameter entirely and inject only services. !!! tip "Cancellation buffers" + The cancellation token fires slightly **before** AWS kills the process: - The runtime subtracts `LambdaHostOptions.InvocationCancellationBuffer` (default 500ms) from the - remaining time when creating the token. + remaining time when creating the token. - Always pass it down to outbound SDK calls and database queries so you can stop work cleanly. ## Middleware and Lifecycle Hooks: Source-Generated DI - Middleware receives the invocation scope via the `ILambdaInvocationContext` argument. Resolve services with - `context.ServiceProvider` or create reusable middleware classes with constructor injection. + `context.ServiceProvider` or create reusable middleware classes with constructor injection. - `OnInit` and `OnShutdown` handlers now use the same source-generated dependency injection as your main - handlers. Each executes inside its own scoped service provider so you can warm caches, seed connections, - or flush telemetry without leaking per-invocation services. + handlers. Each executes inside its own scoped service provider so you can warm caches, seed connections, + or flush telemetry without leaking per-invocation services. OnInit and OnShutdown handlers support multiple dependency injection patterns: @@ -146,11 +147,11 @@ variants work but rarely matter in Lambda because configuration usually ships wi ## Patterns That Work Well - **Constructor injection everywhere** – middleware, handlers, lifecycle hooks can all resolve services - directly. Avoid service locator patterns unless you truly need dynamic lookups. + directly. Avoid service locator patterns unless you truly need dynamic lookups. - **Decorator pattern** – use `builder.Services.Decorate()` (from Scrutor) to add - caching, logging, or retry behavior without touching core services. + caching, logging, or retry behavior without touching core services. - **Keyed services** – register multiple implementations with `AddKeyed{Lifetime}` and inject the - one you need via `[FromKeyedServices]`. + one you need via `[FromKeyedServices]`. ### Keyed Services in Practice @@ -167,12 +168,12 @@ lambda.MapHandler(( - Keys can be strings, enums, numeric types, or even `Type` instances. - Optional services are supported by making the parameter nullable. - The generated code throws a descriptive exception if the service provider doesn't support keyed - services (e.g., if you run on an older DI container). + services (e.g., if you run on an older DI container). ## Host-Specific Pitfalls | Pitfall | Impact | Fix | -|---------------------------------------|-----------------------------------------------------|-----------------------------------------------------------------------------------------| +| ------------------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------- | | Singleton depends on a scoped service | Scoped instance from first invocation leaks forever | Inject `IServiceProvider`, create a scope, resolve the scoped service inside the method | | Storing scoped services in singletons | `ObjectDisposedException` on later invocations | Keep scoped dependencies scoped; pass data instead of services | | Over-injecting handlers | Hard-to-test functions with 8+ services | Move orchestration into services; keep handlers thin | diff --git a/docs/guides/error-handling.md b/docs/guides/error-handling.md index 5b6b6038..ece757ea 100644 --- a/docs/guides/error-handling.md +++ b/docs/guides/error-handling.md @@ -7,12 +7,12 @@ hooks so you can add the right amount of protection without fighting the framewo ## Invocation Errors: What Happens by Default - Handlers run inside the middleware pipeline. If a handler throws and nothing catches the exception, - `MinimalLambda` lets it bubble back through the pipeline. + `MinimalLambda` lets it bubble back through the pipeline. - Once the exception leaves the outermost middleware, the AWS .NET Lambda runtime records the error, - writes it to CloudWatch Logs, and returns a failed invocation (with retries governed by the event - source). + writes it to CloudWatch Logs, and returns a failed invocation (with retries governed by the event + source). - Because the runtime already reports unhandled errors, you only need to add custom handling when you - want different logging, metrics, or response shaping. + want different logging, metrics, or response shaping. ```csharp title="Program.cs" linenums="1" lambda.MapHandler(([FromEvent] OrderRequest request, IOrderService service) => @@ -53,8 +53,8 @@ lambda.UseMiddleware(async (context, next) => - Register error-handling middleware first so it wraps every other component. - Use the helper extensions (`context.GetResponse()`, `context.GetEvent()`, etc.) from - `FeatureLambdaInvocationContextExtensions` (they wrap `ILambdaInvocationContext.Features`) when you need to read - or replace the outgoing payload instead of throwing. + `FeatureLambdaInvocationContextExtensions` (they wrap `ILambdaInvocationContext.Features`) when you need to read + or replace the outgoing payload instead of throwing. - Still rethrow fatal errors so the runtime produces accurate CloudWatch metrics and DLQ/SQS retries. ## Handler-Level Try/Catch @@ -82,12 +82,12 @@ lambda.MapHandler(async ([FromEvent] CheckoutRequest request, ICheckoutService s handler executes in its own DI scope and errors are aggregated: - **OnInit** – All handlers run concurrently with a token derived from - `LambdaHostOptions.InitTimeout`. Each handler can optionally return `bool`. `MinimalLambda` collects - every exception and throws an `AggregateException("Encountered errors while running OnInit handlers", …)` - if any fail. If a handler returns `false`, initialization aborts even when no exception occurred. + `LambdaHostOptions.InitTimeout`. Each handler can optionally return `bool`. `MinimalLambda` collects + every exception and throws an `AggregateException("Encountered errors while running OnInit handlers", …)` + if any fail. If a handler returns `false`, initialization aborts even when no exception occurred. - **OnShutdown** – Handlers also run concurrently. Any exception is captured and rethrown as an - `AggregateException("Encountered errors while running OnShutdown handlers", …)` after every handler - finishes (or faults). Use the provided `CancellationToken` to respect the remaining shutdown window. + `AggregateException("Encountered errors while running OnShutdown handlers", …)` after every handler + finishes (or faults). Use the provided `CancellationToken` to respect the remaining shutdown window. ```csharp title="Program.cs" linenums="1" lambda.OnInit(async (ICache cache, CancellationToken ct) => diff --git a/docs/guides/handler-registration.md b/docs/guides/handler-registration.md index 04ab5267..66d353e9 100644 --- a/docs/guides/handler-registration.md +++ b/docs/guides/handler-registration.md @@ -102,14 +102,14 @@ lambda.MapHandler((ILogger logger) => Handlers can mix lambda events with services, context objects, and cancellation tokens. This table shows what the generator knows how to supply: -| Parameter | Source | -|--------------------------------------------------|-----------------------------------------------------------------------------------------------------| -| `[FromEvent] T event` | Deserialized from the Lambda payload (or envelope). Optional—only include when the handler expects an input. | -| `IServiceType service` | Resolved from the DI container using the invocation scope. | -| `[FromKeyedServices("key")] IServiceType keyed` | Resolves a keyed service registered with `AddKeyed*`. Keys must be constants supported by the BCL. | -| `ILambdaInvocationContext context` | Framework context that extends `ILambdaContext`, exposes scoped `ServiceProvider`, `Items`, `Features`, `Properties`, and the invocation `CancellationToken`. | -| `ILambdaContext lambdaContext` | Raw AWS Lambda context for folks that prefer the SDK contract. | -| `CancellationToken cancellationToken` | Cancels when `InvocationCancellationBuffer` elapses before the Lambda timeout. | +| Parameter | Source | +| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `[FromEvent] T event` | Deserialized from the Lambda payload (or envelope). Optional—only include when the handler expects an input. | +| `IServiceType service` | Resolved from the DI container using the invocation scope. | +| `[FromKeyedServices("key")] IServiceType keyed` | Resolves a keyed service registered with `AddKeyed*`. Keys must be constants supported by the BCL. | +| `ILambdaInvocationContext context` | Framework context that extends `ILambdaContext`, exposes scoped `ServiceProvider`, `Items`, `Features`, `Properties`, and the invocation `CancellationToken`. | +| `ILambdaContext lambdaContext` | Raw AWS Lambda context for folks that prefer the SDK contract. | +| `CancellationToken cancellationToken` | Cancels when `InvocationCancellationBuffer` elapses before the Lambda timeout. | ```csharp title="Program.cs" linenums="1" lambda.MapHandler(async ( diff --git a/docs/guides/hosting.md b/docs/guides/hosting.md index 2825e10d..d2a8dd38 100644 --- a/docs/guides/hosting.md +++ b/docs/guides/hosting.md @@ -10,9 +10,9 @@ Calling `LambdaApplication.CreateBuilder()` assembles a standard .NET host with - **Environment & content root** – Sets `IHostEnvironment.ApplicationName` from `AWS_LAMBDA_FUNCTION_NAME` (when available) and resolves the content root by honoring `DOTNET_CONTENTROOT`, `AWS_LAMBDA_TASK_ROOT`, or falling back to `Directory.GetCurrentDirectory()`. - **Logging** – Registers console logging with activity tracking enabled. In Development, scope validation is turned on so singleton/scoped misuse throws during build. - **Dependency injection** – Every call to `builder.Services` hits the standard `IServiceCollection`. On `builder.Build()`, minimal-lambda registers: - - `ILambdaInvocationBuilderFactory`, `ILambdaOnInitBuilderFactory`, and `ILambdaOnShutdownBuilderFactory` so lambda-specific pipelines can be composed later. - - `LambdaHostedService`, `ILambdaHandlerFactory`, feature collections, and `ILambdaBootstrapOrchestrator`. - - Default implementations of `ILambdaSerializer` (System.Text.Json) and `ILambdaCancellationFactory` unless you already registered your own via `TryAddLambdaHostDefaultServices()`. + - `ILambdaInvocationBuilderFactory`, `ILambdaOnInitBuilderFactory`, and `ILambdaOnShutdownBuilderFactory` so lambda-specific pipelines can be composed later. + - `LambdaHostedService`, `ILambdaHandlerFactory`, feature collections, and `ILambdaBootstrapOrchestrator`. + - Default implementations of `ILambdaSerializer` (System.Text.Json) and `ILambdaCancellationFactory` unless you already registered your own via `TryAddLambdaHostDefaultServices()`. Most applications can rely entirely on `CreateBuilder()` + `builder.Build()`—just add services, middleware, handlers, and call `await lambda.RunAsync();`. @@ -101,12 +101,12 @@ handlers. ## Troubleshooting -| Issue | Cause | Fix | -|-------|-------|-----| -| `InvalidOperationException: Lambda Handler is not set.` | `builder.Build()` succeeded but `lambda.MapHandler(...)` was never called. | Register a handler before calling `lambda.RunAsync()`. -| `AggregateException: Encountered errors while running OnInit handlers` | An OnInit delegate threw or returned `false`. | Inspect inner exceptions; ensure handlers honor cancellation and only return `false` for fatal conditions. -| `Graceful shutdown ... did not complete within the allocated timeout` | OnShutdown handlers exceeded `LambdaHostOptions.ShutdownDuration - ShutdownDurationBuffer`. | Reduce work, increase shutdown duration, or skip optional cleanup. -| Environment variables not loaded | You disabled defaults without re-adding `builder.Configuration.AddEnvironmentVariables()`. | Re-add configuration sources or keep defaults. +| Issue | Cause | Fix | +| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| `InvalidOperationException: Lambda Handler is not set.` | `builder.Build()` succeeded but `lambda.MapHandler(...)` was never called. | Register a handler before calling `lambda.RunAsync()`. | +| `AggregateException: Encountered errors while running OnInit handlers` | An OnInit delegate threw or returned `false`. | Inspect inner exceptions; ensure handlers honor cancellation and only return `false` for fatal conditions. | +| `Graceful shutdown ... did not complete within the allocated timeout` | OnShutdown handlers exceeded `LambdaHostOptions.ShutdownDuration - ShutdownDurationBuffer`. | Reduce work, increase shutdown duration, or skip optional cleanup. | +| Environment variables not loaded | You disabled defaults without re-adding `builder.Configuration.AddEnvironmentVariables()`. | Re-add configuration sources or keep defaults. | ## Related Guides diff --git a/docs/guides/index.md b/docs/guides/index.md index b3d3566f..26fcf880 100644 --- a/docs/guides/index.md +++ b/docs/guides/index.md @@ -7,6 +7,7 @@ Comprehensive guides for building production Lambda functions with `MinimalLambd Master the essential framework features that power your Lambda functions. ### [Dependency Injection](dependency-injection.md) + Learn service registration patterns, understand Singleton vs Scoped lifetimes, and master dependency injection in handlers and lifecycle methods. **Topics covered:** @@ -18,6 +19,7 @@ Learn service registration patterns, understand Singleton vs Scoped lifetimes, a - Best practices and anti-patterns ### [Middleware](middleware.md) + Build middleware pipelines for cross-cutting concerns like logging, metrics, validation, and error handling. **Topics covered:** @@ -29,6 +31,7 @@ Build middleware pipelines for cross-cutting concerns like logging, metrics, val - Reusable middleware components ### [Lifecycle Management](lifecycle-management.md) + Understand and control the Lambda lifecycle phases: OnInit, Invocation, and OnShutdown. **Topics covered:** @@ -40,6 +43,7 @@ Understand and control the Lambda lifecycle phases: OnInit, Invocation, and OnSh - Error handling in lifecycle ### [Handler Registration](handler-registration.md) + Register type-safe Lambda handlers with automatic dependency injection and source generation. **Topics covered:** @@ -52,6 +56,7 @@ Register type-safe Lambda handlers with automatic dependency injection and sourc - Handler patterns ### [Hosting & Builder](hosting.md) + Understand what `LambdaApplication.CreateBuilder()` configures, how the runtime composes middleware, and how to customize the host for advanced scenarios. @@ -62,7 +67,9 @@ and how to customize the host for advanced scenarios. - LambdaHostedService orchestration - Default serializers and cancellation factories - Troubleshooting host setup + ### [Configuration](configuration.md) + Configure framework behavior with LambdaHostOptions and application settings. **Topics covered:** @@ -79,6 +86,7 @@ Configure framework behavior with LambdaHostOptions and application settings. Build robust, testable, and deployable Lambda functions. ### [Error Handling](error-handling.md) + Implement resilient error handling with retries, graceful degradation, and proper exception management. **Topics covered:** @@ -91,6 +99,7 @@ Implement resilient error handling with retries, graceful degradation, and prope - Best practices ### [Testing](testing.md) + Write comprehensive tests for your Lambda functions using xUnit, NSubstitute, and AutoFixture. **Topics covered:** @@ -101,7 +110,7 @@ Write comprehensive tests for your Lambda functions using xUnit, NSubstitute, an - Testing handlers and middleware - Integration testing - Test naming conventions - - In-memory end-to-end testing with `MinimalLambda.Testing` (`WebApplicationFactory`-style runtime shim) +- In-memory end-to-end testing with `MinimalLambda.Testing` (`WebApplicationFactory`-style runtime shim) ## Learning Path @@ -130,6 +139,6 @@ If you encounter issues not covered in these guides: - Search or ask in [GitHub Discussions](https://github.com/j-d-ha/minimal-lambda/discussions) - Report bugs in [GitHub Issues](https://github.com/j-d-ha/minimal-lambda/issues) ---- +______________________________________________________________________ Ready to dive in? Choose a guide above or start with [Dependency Injection](dependency-injection.md). diff --git a/docs/guides/lifecycle-management.md b/docs/guides/lifecycle-management.md index 5d2cb3b6..723c8522 100644 --- a/docs/guides/lifecycle-management.md +++ b/docs/guides/lifecycle-management.md @@ -210,21 +210,21 @@ The `Properties` dictionary is backed by a thread-safe `ConcurrentDictionary` | Thread-safe dictionary for sharing data between handlers or from OnInit to invocation handlers | -| `ElapsedTime` | `TimeSpan` | Time elapsed since the Lambda execution environment started | -| `Region` | `string?` | AWS region where the function is running (from `AWS_REGION` env var) | -| `ExecutionEnvironment` | `string?` | Runtime identifier like `AWS_Lambda_dotnet8` (from `AWS_EXECUTION_ENV` env var) | -| `FunctionName` | `string?` | Name of the Lambda function (from `AWS_LAMBDA_FUNCTION_NAME` env var) | -| `FunctionMemorySize` | `int?` | Memory allocated to the function in MB (from `AWS_LAMBDA_FUNCTION_MEMORY_SIZE` env var) | -| `FunctionVersion` | `string?` | Version of the function being executed (from `AWS_LAMBDA_FUNCTION_VERSION` env var) | -| `InitializationType` | `string?` | Type of initialization: `on-demand`, `provisioned-concurrency`, `snap-start`, or `lambda-managed-instances` (from `AWS_LAMBDA_INITIALIZATION_TYPE` env var) | -| `LogGroupName` | `string?` | CloudWatch Logs group name (from `AWS_LAMBDA_LOG_GROUP_NAME` env var, not available in SnapStart) | -| `LogStreamName` | `string?` | CloudWatch Logs stream name (from `AWS_LAMBDA_LOG_STREAM_NAME` env var, not available in SnapStart) | -| `TaskRoot` | `string?` | Path to the Lambda function code (from `LAMBDA_TASK_ROOT` env var) | +| Property | Type | Description | +| ---------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `CancellationToken` | `CancellationToken` | Signals cancellation when `InitTimeout` (OnInit) or `ShutdownDuration - ShutdownDurationBuffer` (OnShutdown) elapses, or when SIGTERM is received | +| `ServiceProvider` | `IServiceProvider` | The scoped service container for this handler invocation. Use for manual service resolution when direct injection isn't sufficient | +| `Properties` | `IDictionary` | Thread-safe dictionary for sharing data between handlers or from OnInit to invocation handlers | +| `ElapsedTime` | `TimeSpan` | Time elapsed since the Lambda execution environment started | +| `Region` | `string?` | AWS region where the function is running (from `AWS_REGION` env var) | +| `ExecutionEnvironment` | `string?` | Runtime identifier like `AWS_Lambda_dotnet8` (from `AWS_EXECUTION_ENV` env var) | +| `FunctionName` | `string?` | Name of the Lambda function (from `AWS_LAMBDA_FUNCTION_NAME` env var) | +| `FunctionMemorySize` | `int?` | Memory allocated to the function in MB (from `AWS_LAMBDA_FUNCTION_MEMORY_SIZE` env var) | +| `FunctionVersion` | `string?` | Version of the function being executed (from `AWS_LAMBDA_FUNCTION_VERSION` env var) | +| `InitializationType` | `string?` | Type of initialization: `on-demand`, `provisioned-concurrency`, `snap-start`, or `lambda-managed-instances` (from `AWS_LAMBDA_INITIALIZATION_TYPE` env var) | +| `LogGroupName` | `string?` | CloudWatch Logs group name (from `AWS_LAMBDA_LOG_GROUP_NAME` env var, not available in SnapStart) | +| `LogStreamName` | `string?` | CloudWatch Logs stream name (from `AWS_LAMBDA_LOG_STREAM_NAME` env var, not available in SnapStart) | +| `TaskRoot` | `string?` | Path to the Lambda function code (from `LAMBDA_TASK_ROOT` env var) | All AWS environment properties are nullable because they depend on environment variables set by the Lambda runtime. Most are available during normal execution, but some (like `LogGroupName` and `LogStreamName`) are unavailable in SnapStart functions. diff --git a/docs/guides/middleware.md b/docs/guides/middleware.md index 8d03ed6a..85c78ba6 100644 --- a/docs/guides/middleware.md +++ b/docs/guides/middleware.md @@ -9,7 +9,8 @@ access, and composition tips that keep middleware and handlers decoupled without ## Pipeline Basics -Register middleware before calling `MapHandler`. Components execute in registration order and unwind in +Register middleware before calling `MapHandler`. Components execute in registration order and unwind +in reverse order: ```csharp title="Program.cs" @@ -73,11 +74,11 @@ Key members: - `ServiceProvider` – resolve scoped services for the invocation. - `CancellationToken` – fires before Lambda termination (buffer controlled by - `LambdaHostOptions.InvocationCancellationBuffer`). Pass it to downstream async work. + `LambdaHostOptions.InvocationCancellationBuffer`). Pass it to downstream async work. - `Items` – per-invocation storage shared by middleware/handler. - `Properties` – cross-invocation storage. - `Features` – typed capabilities such as `IEventFeature` and `IResponseFeature` that let - middleware collaborate without injecting each other. + middleware collaborate without injecting each other. ## Middleware Approaches @@ -136,9 +137,9 @@ lambda.UseMiddleware(async (context, next) => - **Dependency Injection** - `context.ServiceProvider.GetRequiredService()` - **Event/Response Data** - `GetEvent()`, `GetResponse()`, `TryGetEvent()` ( - see [Type-Safe Feature Access](#type-safe-feature-access)) + see [Type-Safe Feature Access](#type-safe-feature-access)) - **Features** - `context.Features.Get>()` ( - see [Working with Features](#working-with-features)) + see [Working with Features](#working-with-features)) - **Per-Invocation State** - `context.Items` for temporary data within the request - **Cross-Invocation State** - `context.Properties` for data shared across Lambda invocations - **Cancellation** - `context.CancellationToken` for cooperative cancellation @@ -202,9 +203,10 @@ that instantiates your middleware and resolves constructor parameters automatica no runtime overhead. !!! tip "Reusable packages" -Class-based middleware is a good fit for shared packages: ship the middleware type and attributes, -and the consuming app's build generates the wiring code. The generated code lives in the -application's build output, not in your package. + + Class-based middleware is a good fit for shared packages: ship the middleware type and attributes, + and the consuming app's build generates the wiring code. The generated code lives in the + application's build output, not in your package. #### Dependency Injection @@ -255,10 +257,10 @@ internal sealed class ValidationMiddleware : ILambdaMiddleware **Default resolution behavior:** - Parameters without attributes first check args passed to `UseMiddleware()`, then fall back to - DI + DI - Services must be registered in `builder.Services` before calling `builder.Build()` - Use `[FromServices]` to skip args and resolve directly from DI ( - see [Parameter Sources](#parameter-sources)) + see [Parameter Sources](#parameter-sources)) For more on service lifetimes and DI patterns, see [Dependency Injection](dependency-injection.md). @@ -296,7 +298,7 @@ await lambda.RunAsync(); Control how constructor parameters are resolved using attributes: | Attribute | Source | Behavior | -|-----------------------|---------------|---------------------------------------------------------| +| --------------------- | ------------- | ------------------------------------------------------- | | (none) | Args, then DI | Try args first, fall back to DI if no match | | `[FromServices]` | DI only | Resolve from DI container, skip args | | `[FromKeyedServices]` | Keyed DI | Resolve keyed service from DI (e.g., `"primary"` cache) | @@ -368,10 +370,11 @@ lambda.MapHandler(([FromEvent] OrderRequest req) => ProcessOrder(req)); await lambda.RunAsync(); ``` -!!! tip "When to use [FromArguments]" -Use `[FromArguments]` for configuration values that vary per middleware registration (like -cache keys, API endpoints, or feature flags). This makes the middleware reusable with -different configurations. +!!! tip "When to use `[FromArguments]`" + + Use `[FromArguments]` for configuration values that vary per middleware registration (like + cache keys, API endpoints, or feature flags). This makes the middleware reusable with + different configurations. #### Multiple Constructors @@ -429,8 +432,9 @@ internal sealed class AuthMiddleware : ILambdaMiddleware ``` !!! warning -Only one constructor can have `[MiddlewareConstructor]`. Applying it to multiple constructors -triggers compile-time diagnostic **LH0005**. + + Only one constructor can have `[MiddlewareConstructor]`. Applying it to multiple constructors + triggers compile-time diagnostic **LH0005**. #### Lifecycle and Disposal @@ -488,9 +492,10 @@ internal sealed class TracingMiddleware : ILambdaMiddleware, IAsyncDisposable - The generated code prefers `IAsyncDisposable` over `IDisposable` if both are implemented !!! info "Singleton vs. Per-Invocation" -Unlike services registered in DI (which can be singleton, scoped, or transient), middleware -instances are **always per-invocation**. For shared state, inject singleton services into the -middleware constructor. + + Unlike services registered in DI (which can be singleton, scoped, or transient), middleware + instances are **always per-invocation**. For shared state, inject singleton services into the + middleware constructor. For more on lifecycle hooks, see [Lifecycle Management](lifecycle-management.md). @@ -571,8 +576,9 @@ internal async Task InvokeAsync_ReturnsCachedResult_WhenCacheHit() ``` !!! tip "Testing Strategy" -Test middleware in isolation by mocking `ILambdaInvocationContext` and the `next` delegate. -This keeps tests fast and focused on middleware behavior without spinning up the entire pipeline. + + Test middleware in isolation by mocking `ILambdaInvocationContext` and the `next` delegate. + This keeps tests fast and focused on middleware behavior without spinning up the entire pipeline. For more testing patterns, see [Testing](testing.md). @@ -835,7 +841,7 @@ internal sealed class MaintenanceModeMiddleware : ILambdaMiddleware The source generator validates middleware at compile-time: | Diagnostic | Severity | Description | -|------------|----------|-------------------------------------------------------------------------| +| ---------- | -------- | ----------------------------------------------------------------------- | | **LH0005** | Error | Multiple constructors have `[MiddlewareConstructor]` (only one allowed) | | **LH0006** | Error | Middleware type must be a concrete class (not interface/abstract) | @@ -853,13 +859,15 @@ lambda.UseMiddleware(); // Concrete class ``` !!! info "Compile-Time Safety" -These diagnostics catch configuration errors during build, not at runtime. This prevents -deployment of misconfigured middleware. + + These diagnostics catch configuration errors during build, not at runtime. This prevents + deployment of misconfigured middleware. ## Working with Features Features are type-keyed adapters stored inside `ILambdaInvocationContext.Features` (an -`IFeatureCollection`). They decouple middleware from handlers: a handler (or the framework) populates a +`IFeatureCollection`). They decouple middleware from handlers: a handler (or the framework) +populates a feature, middleware reads or mutates it, and nobody needs to inject each other through DI. The collection lazily creates features by asking every registered `IFeatureProvider` to build them when first requested. @@ -884,7 +892,7 @@ lambda.UseMiddleware(async (context, next) => Common features: | Feature | Purpose | -|-------------------------------|--------------------------------------------------------------| +| ----------------------------- | ------------------------------------------------------------ | | `IEventFeature` | Access the deserialized event payload | | `IResponseFeature` | Inspect or replace the handler response before serialization | | `IInvocationDataFeature` | Access raw event/response streams for envelopes | @@ -892,12 +900,15 @@ Common features: **Why features matter:** - Middleware can extract values set by handlers (or other middleware) without DI fan-out. -- Handlers remain free of middleware-specific dependencies; they just work with the event/response types. -- Custom features are easy to add—register an implementation of `IFeatureProvider` and it becomes available to all middleware. +- Handlers remain free of middleware-specific dependencies; they just work with the event/response + types. +- Custom features are easy to add—register an implementation of `IFeatureProvider` and it becomes + available to all middleware. ### Type-Safe Feature Access -The framework provides convenient extension methods on `ILambdaInvocationContext` for type-safe event and response access, simplifying the feature access pattern shown above: +The framework provides convenient extension methods on `ILambdaInvocationContext` for type-safe +event and response access, simplifying the feature access pattern shown above: ```csharp title="Program.cs" lambda.UseMiddleware(async (context, next) => @@ -925,7 +936,7 @@ lambda.UseMiddleware(async (context, next) => **Available Methods:** | Method | Description | Returns | -|----------------------------|-----------------------------------------|------------------------------------------| +| -------------------------- | --------------------------------------- | ---------------------------------------- | | `GetEvent()` | Returns event or `null` if not found | `T?` | | `GetResponse()` | Returns response or `null` if not found | `T?` | | `TryGetEvent(out T)` | Try-pattern for safe event access | `bool` | @@ -935,15 +946,20 @@ lambda.UseMiddleware(async (context, next) => **When to use each:** -- **Nullable methods** (`GetEvent()`) – When the event/response might not exist and you'll handle null gracefully -- **Try pattern** (`TryGetEvent()`) – When you want explicit null checking without additional conditionals -- **Required methods** (`GetRequiredEvent()`) – When the event/response must exist and missing it is an error condition +- **Nullable methods** (`GetEvent()`) – When the event/response might not exist and you'll handle + null gracefully +- **Try pattern** (`TryGetEvent()`) – When you want explicit null checking without additional + conditionals +- **Required methods** (`GetRequiredEvent()`) – When the event/response must exist and missing it + is an error condition -These methods are equivalent to calling `context.Features.Get>()` and accessing the event/response, but provide cleaner syntax and better null-safety annotations. +These methods are equivalent to calling `context.Features.Get>()` and accessing the +event/response, but provide cleaner syntax and better null-safety annotations. ### Feature Providers in Practice -When `context.Features.Get()` runs, `MinimalLambda` walks through every registered `IFeatureProvider` +When `context.Features.Get()` runs, `MinimalLambda` walks through every registered +`IFeatureProvider` until one returns the requested feature. Built-in providers handle common cases such as response serialization. Use the same pattern for your features. @@ -1090,14 +1106,17 @@ Both approaches access the same options registered in `builder.Services.Configur **General:** - **Keep middleware focused.** One responsibility per component (logging, metrics, caching, etc.). -- **Always call `await next(context)`** unless you intentionally short-circuit; forgetting it prevents the - handler from running. +- **Always call `await next(context)`** unless you intentionally short-circuit; forgetting it + prevents the + handler from running. - **Never swallow exceptions silently.** If you handle an error, set a response or log it so Lambda - doesn't - report success unintentionally. -- **Use per-invocation state wisely.** `Items` is cleared after each request; `Properties` live for the life - of the container and must be thread-safe. -- **Make cancellation cooperative.** Honor `context.CancellationToken` in middleware and pass it to downstream I/O. + doesn't + report success unintentionally. +- **Use per-invocation state wisely.** `Items` is cleared after each request; `Properties` live for + the life + of the container and must be thread-safe. +- **Make cancellation cooperative.** Honor `context.CancellationToken` in middleware and pass it to + downstream I/O. **Inline Middleware:** @@ -1116,12 +1135,13 @@ Both approaches access the same options registered in `builder.Services.Configur **Choosing Between Inline and Class-Based:** | Use Inline When... | Use Class-Based When... | -|--------------------------------------------|---------------------------------------------------| +| ------------------------------------------ | ------------------------------------------------- | | Middleware is application-specific | Middleware will be reused across projects | | Logic is simple orchestration | Logic is complex or has multiple responsibilities | | No disposal or lifecycle management needed | Need `IDisposable` or `IAsyncDisposable` support | | Quickly prototyping or experimenting | Ready to formalize and test thoroughly | | Tight coupling to app logic is acceptable | Clean separation of concerns is important | -With these patterns, you can build rich, testable pipelines around your Lambda handlers while keeping +With these patterns, you can build rich, testable pipelines around your Lambda handlers while +keeping business logic small and focused. diff --git a/docs/guides/testing.md b/docs/guides/testing.md index 6b0698a4..5a48dd5c 100644 --- a/docs/guides/testing.md +++ b/docs/guides/testing.md @@ -8,11 +8,11 @@ lifecycle hooks, DI) without deploying or opening ports. ## When to Use - **End-to-end pipeline coverage** – Exercise source-generated handlers, middleware, envelopes, and - lifecycle hooks with real DI and serialization. + lifecycle hooks with real DI and serialization. - **Regression nets** – Verify bootstrapping, cold-start logic, and error payloads stay stable. - **Host customization** – Override configuration/services per test via `WithHostBuilder`. - Prefer plain unit tests for isolated logic; reach for MinimalLambda.Testing when you need - confidence in the Lambda runtime behavior. + confidence in the Lambda runtime behavior. ## Quick Start @@ -24,6 +24,7 @@ dotnet add package MinimalLambda.Testing ``` !!! warning "Package Versions" + Ensure `MinimalLambda.Testing` version matches your `MinimalLambda` version. Mismatched versions may cause runtime errors or unexpected behavior. @@ -62,11 +63,11 @@ public class HelloWorldTests ## Invocation APIs - `InvokeAsync(event, token)` – Send a strongly typed event, expect a typed - response; fails with an `InvocationResponse` containing error details on handler - exceptions. + response; fails with an `InvocationResponse` containing error details on handler + exceptions. - `InvokeNoEventAsync(token)` – Invoke a handler that does not take an event payload. - `InvokeNoResponseAsync(event, token)` – Fire-and-forget style; skips response - deserialization for handlers that return `void`/`Task` or write directly to streams. + deserialization for handlers that return `void`/`Task` or write directly to streams. `InvocationResponse`/`InvocationResponse` include `WasSuccess`, `Response`, and structured `Error` information that mirrors Lambda runtime error payloads—assert on these to verify failures. @@ -90,15 +91,15 @@ If omitted, a new GUID is generated for each invocation. ## Working with Cancellation - **Propagate test cancellation** – Call `WithCancellationToken(...)` on the factory to flow your - test framework's token into the in-memory runtime. All server operations observe it. + test framework's token into the in-memory runtime. All server operations observe it. - **Per-call tokens** – Pass tokens to `StartAsync` and `Invoke*` to bound individual operations. - **Pre-canceled tokens** – A pre-canceled token will fail the invocation immediately (see - `SimpleLambda_WithPreCanceledToken_CancelsInvocation` in the test suite). + `SimpleLambda_WithPreCanceledToken_CancelsInvocation` in the test suite). - **Automatic timeouts** – Every invocation automatically times out after - `LambdaServerOptions.FunctionTimeout` (defaults to 3 seconds, matching AWS Lambda's default). - The test server creates a linked cancellation token for each invocation that enforces this deadline, - mirroring Lambda's actual timeout behavior. Adjust `factory.ServerOptions.FunctionTimeout` before - invoking to test different timeout scenarios or catch slow handlers. + `LambdaServerOptions.FunctionTimeout` (defaults to 3 seconds, matching AWS Lambda's default). + The test server creates a linked cancellation token for each invocation that enforces this deadline, + mirroring Lambda's actual timeout behavior. Adjust `factory.ServerOptions.FunctionTimeout` before + invoking to test different timeout scenarios or catch slow handlers. ## Host Customization and Fixtures @@ -174,7 +175,7 @@ public class SimpleLambdaTests(LambdaApplicationFactory factory) - OnShutdown runs once when all tests complete - Singleton services are shared across all tests in the class - Do not use this pattern if you need to test initialization/shutdown behavior (use a fresh factory - per test instead) + per test instead) #### Custom Factory for Reusable Configuration @@ -243,7 +244,7 @@ Access it via `factory.ServerOptions` before starting the server if you need tes - **Runtime headers** – `FunctionArn`, `AdditionalHeaders` for custom Lambda runtime headers - **Timeout behavior** – `FunctionTimeout` controls invocation deadline (defaults to 3 seconds) - **JSON serialization** – `SerializerOptions` controls how the test server serializes events and - responses sent to your handler + responses sent to your handler ```csharp linenums="1" await using var factory = new LambdaApplicationFactory(); @@ -266,14 +267,15 @@ var response = await factory.TestServer.InvokeAsync( ## Initialization and Shutdown Behavior - `StartAsync` returns `InitResponse` with `InitStatus` values: - - `InitCompleted` / `InitAlreadyCompleted` – Ready to invoke. - - `InitError` – An `ErrorResponse` from OnInit failures; server stops itself. - - `HostExited` – Entry point exited early (e.g., OnInit signaled stop). + - `InitCompleted` / `InitAlreadyCompleted` – Ready to invoke. + - `InitError` – An `ErrorResponse` from OnInit failures; server stops itself. + - `HostExited` – Entry point exited early (e.g., OnInit signaled stop). - `Invoke*` will start the server on-demand; if init fails it throws with the reported status. - `StopAsync` triggers OnShutdown and aggregates any exceptions (surfaced as `AggregateException`). - `DisposeAsync` is idempotent; safe to call multiple times. !!! tip "StartAsync is Optional" + `InvokeAsync` will automatically call `StartAsync` if you haven't called it explicitly. **When to call StartAsync explicitly:** @@ -498,20 +500,21 @@ public async Task ColdStart_InitCompletesWithinTimeout() ## Best Practices - **Reuse factories per class** – Creating a new factory per test is fine; reuse within a class to - speed up suites that share the same host configuration. + speed up suites that share the same host configuration. - **Runtime headers** – Responses include the same headers Lambda sends (`Lambda-Runtime-*` plus any - `AdditionalHeaders` you set); assert on them if you need to prove deadline/ARN behavior. + `AdditionalHeaders` you set); assert on them if you need to prove deadline/ARN behavior. - **Fresh factory per test for lifecycle testing** – When testing OnInit/OnShutdown, create a new - factory per test so lifecycle hooks run predictably. + factory per test so lifecycle hooks run predictably. !!! warning "Fixture Reuse Pitfalls" + - Using `IClassFixture`/`ICollectionFixture` with a single `LambdaApplicationFactory` means one - host instance is shared across all tests in that scope. Avoid this pattern if you need to test - startup/shutdown logic—use a fresh factory per test so OnInit/OnShutdown run predictably. + host instance is shared across all tests in that scope. Avoid this pattern if you need to test + startup/shutdown logic—use a fresh factory per test so OnInit/OnShutdown run predictably. - Do not mix a fixture-based factory with new factories created inside individual tests; they can - overlap and run simultaneously, leading to multiple hosts executing in parallel and surprising - side effects. Choose one approach (per-test or shared fixture) for a given test class/collection - and clean up via `DisposeAsync`/`StopAsync` when done. + overlap and run simultaneously, leading to multiple hosts executing in parallel and surprising + side effects. Choose one approach (per-test or shared fixture) for a given test class/collection + and clean up via `DisposeAsync`/`StopAsync` when done. ## Complete Examples diff --git a/docs/index.md b/docs/index.md index 791e0100..376dbbc6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,5 +1,5 @@ --- -title: "" +title: '' --- # MinimalLambda: ASP.NET Core Patterns for AWS Lambda @@ -21,7 +21,7 @@ smoother developer experience when iterating locally or in CI. [Guides](guides/index.md){ .md-button } [Examples (Coming Soon)](examples/index.md){ .md-button } ---- +______________________________________________________________________ ## Why MinimalLambda? @@ -107,7 +107,7 @@ already know, while still embracing Lambda’s execution model. await lambda.RunAsync(); ``` ---- +______________________________________________________________________ ## Key Features @@ -155,7 +155,7 @@ Small abstraction surface area keeps CPU and memory usage predictable inside Lam [Advanced topics (Coming Soon)](advanced/index.md){ .md-button } ---- +______________________________________________________________________ ## Quick Start @@ -179,10 +179,11 @@ await lambda.RunAsync(); ``` !!! tip "Next Steps" -Ready to dive deeper? Check out the [Getting Started Guide](getting-started/index.md) for a complete -tutorial, or explore the [Examples](examples/index.md) to see real-world applications. ---- + Ready to dive deeper? Check out the [Getting Started Guide](getting-started/index.md) for a complete + tutorial, or explore the [Examples](examples/index.md) to see real-world applications. + +______________________________________________________________________ ## Packages @@ -192,7 +193,7 @@ The core packages provide the fundamental hosting framework, abstractions, and o for building AWS Lambda functions. | Package | Description | NuGet | Downloads | -|---------------------------------------------------------------------------------------------------------------------|-----------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------| +| ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | [**MinimalLambda**](https://github.com/j-d-ha/minimal-lambda/tree/main/src/MinimalLambda) | Core hosting framework with middleware and DI | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.svg)](https://www.nuget.org/packages/MinimalLambda) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.svg)](https://www.nuget.org/packages/MinimalLambda/) | | [**MinimalLambda.Abstractions**](https://github.com/j-d-ha/minimal-lambda/tree/main/src/MinimalLambda.Abstractions) | Core interfaces and contracts | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions/) | | [**MinimalLambda.OpenTelemetry**](features/open_telemetry.md) | Distributed tracing and observability | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry/) | @@ -203,14 +204,15 @@ Envelope packages provide type-safe handling of AWS Lambda event sources with au deserialization. !!! info "What are Envelopes?" -Envelopes wrap AWS Lambda events with strongly-typed payload handling, giving you compile-time type -safety and automatic deserialization of message bodies from SQS, SNS, Kinesis, and other event -sources. + + Envelopes wrap AWS Lambda events with strongly-typed payload handling, giving you compile-time type + safety and automatic deserialization of message bodies from SQS, SNS, Kinesis, and other event + sources. [Learn more about envelopes](features/envelopes.md){ .md-button } | Package | Description | NuGet | Downloads | -|---------------------------------------------|-------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| ------------------------------------------- | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **MinimalLambda.Envelopes** | Infrastructure package for HTTP response builders | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes/) | | **MinimalLambda.Envelopes.Sqs** | Simple Queue Service events with typed message bodies | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs/) | | **MinimalLambda.Envelopes.Sns** | Simple Notification Service messages | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns/) | @@ -223,7 +225,7 @@ sources. [Browse all envelope packages](features/envelopes.md){ .md-button } ---- +______________________________________________________________________ ## Examples & Use Cases @@ -233,32 +235,32 @@ soon) for end-to-end Lambda samples that wire up middleware, envelopes, and DI. [Examples (Coming Soon)](examples/index.md){ .md-button } ---- +______________________________________________________________________ ## Community & Resources ### Get Involved - **[GitHub Repository](https://github.com/j-d-ha/minimal-lambda)** – Source code, issues, and - discussions. + discussions. - **[License](https://github.com/j-d-ha/minimal-lambda/blob/main/LICENSE)** – MIT License. ### Documentation - **[Getting Started](getting-started/index.md)** – Installation and first Lambda tutorial. - **[Guides](guides/index.md)** – In-depth docs on DI, middleware, lifecycle, configuration, and - more. + more. - **[Features](features/index.md)** – Envelopes, OpenTelemetry integration, and other add-ons. - **[Advanced Topics](advanced/index.md)** – Coming soon: AOT, source generators, performance - tuning. + tuning. ### Support - Ask or search in [GitHub Discussions](https://github.com/j-d-ha/minimal-lambda/discussions). - File bugs or feature requests - via [GitHub Issues](https://github.com/j-d-ha/minimal-lambda/issues). + via [GitHub Issues](https://github.com/j-d-ha/minimal-lambda/issues). ---- +______________________________________________________________________ **Ready to modernize your Lambda development?** [Get started now](getting-started/index.md){ .md-button .md-button--primary } diff --git a/format.json b/format.json new file mode 100644 index 00000000..700d7711 --- /dev/null +++ b/format.json @@ -0,0 +1,66 @@ +{ + "$schema": "https://raw.githubusercontent.com/j-d-ha/format/main/format.schema.json", + "version": 1, + "formatters": [ + { + "name": "dotnet cleanupcode", + "patterns": [ + "**/*.cs", + "**/*.csx", + "**/*.csproj", + "**/*.props" + ], + "command": [ + "dotnet", + "tool", + "run", + "jb", + "cleanupcode", + "--profile=Built-in: Reformat Code", + "--include=$FILES", + "--settings=$GLOB_FIRST_BASENAME(*.DotSettings)", + "--verbosity=FATAL" + ], + "filesDelimiter": ";" + }, + { + "name": "mdformat mkdocs", + "patterns": [ + "**/docs/**/*.md" + ], + "exclude": [ + ".agents/**", + ".claude/**" + ], + "command": [ + "uvx", + "--with=mdformat-mkdocs", + "--with=mdformat-front-matters", + "--with=mdformat-tables", + "mdformat", + "--number", + "$FILES" + ] + }, + { + "name": "mdformat github markdown", + "patterns": [ + "**/*.md" + ], + "exclude": [ + "docs/**/*.md", + ".agents/**", + ".claude/**" + ], + "command": [ + "uvx", + "--with=mdformat-gfm", + "--with=mdformat-front-matters", + "--with=mdformat-tables", + "mdformat", + "--number", + "$FILES" + ] + } + ] +} diff --git a/mkdocs.yml b/mkdocs.yml index 1de607d3..9defc833 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -36,7 +36,7 @@ theme: toggle: icon: material/lightbulb-auto name: Switch to light mode - + # Palette toggle for light mode - scheme: default primary: orange @@ -45,8 +45,7 @@ theme: toggle: icon: material/lightbulb name: Switch to dark mode - - + # Palette toggle for dark mode - scheme: slate primary: black @@ -93,6 +92,10 @@ markdown_extensions: watch: - ./docs/ +exclude_docs: | + AGENTS.md + CLAUDE.md + nav: - Home: - index.md diff --git a/pyproject.toml b/pyproject.toml index 814b8aff..35f251c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,5 +6,6 @@ requires-python = ">=3.13" [dependency-groups] dev = [ - "zensical>=0.0.33", + "mkdocs-material>=9.7.6", + "zensical>=0.0.40", ] diff --git a/src/AGENTS.md b/src/AGENTS.md new file mode 100644 index 00000000..d65c4428 --- /dev/null +++ b/src/AGENTS.md @@ -0,0 +1,42 @@ +# AGENTS.md (MinimalLambda src) + +## Runtime constraints + +Code under `src/` is Lambda-first and should stay AOT/trimming-friendly. + +- Avoid reflection-heavy or dynamic code unless required and guarded. +- Guard dynamic paths with runtime capability checks when needed. +- Minimize allocations on hot paths. +- Prefer `sealed` for public classes unless inheritance is intended. +- Prefer `internal` for implementation details. +- Use `ArgumentNullException.ThrowIfNull(arg)` for null guards. +- Use `InvalidOperationException` for invalid runtime state. +- Follow local `ConfigureAwait(false)` patterns. + +## C# syntax + +C# 14 extension blocks are valid syntax. Do not rewrite them to old `this` extension methods. + +```csharp +public static class MyExtensions +{ + extension(string value) + { + public int WordCount() => value.Split().Length; + } +} +``` + +Rules: + +- Extension blocks go inside static classes. +- Use `extension(Type receiver)` syntax. +- Members inside access receiver directly. +- Properties/operators are supported. +- `extension(ref Type receiver)` is valid for value types. + +## Source generators + +- Keep generated output deterministic. +- Avoid APIs that break trimming/AOT unless guarded. +- Snapshot updates live under `tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/`. diff --git a/src/CLAUDE.md b/src/CLAUDE.md new file mode 120000 index 00000000..47dc3e3d --- /dev/null +++ b/src/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/src/MinimalLambda.Testing/HostFactoryResolver.cs b/src/MinimalLambda.Testing/HostFactoryResolver.cs index 924f3066..674b62b6 100644 --- a/src/MinimalLambda.Testing/HostFactoryResolver.cs +++ b/src/MinimalLambda.Testing/HostFactoryResolver.cs @@ -40,12 +40,9 @@ private static TimeSpan SetupDefaultTimeout() if (Debugger.IsAttached) return Timeout.InfiniteTimeSpan; - if ( - uint.TryParse( + if (uint.TryParse( Environment.GetEnvironmentVariable(TimeoutEnvironmentKey), - out var timeoutInSeconds - ) - ) + out var timeoutInSeconds)) return TimeSpan.FromSeconds((int)timeoutInSeconds); return TimeSpan.FromMinutes(5); @@ -55,12 +52,12 @@ out var timeoutInSeconds ResolveFactory(assembly, BuildWebHost); public static Func? ResolveWebHostBuilderFactory( - Assembly assembly - ) => ResolveFactory(assembly, CreateWebHostBuilder); + Assembly assembly) => + ResolveFactory(assembly, CreateWebHostBuilder); public static Func? ResolveHostBuilderFactory( - Assembly assembly - ) => ResolveFactory(assembly, CreateHostBuilder); + Assembly assembly) => + ResolveFactory(assembly, CreateHostBuilder); // This helpers encapsulates all of the complex logic required to: // 1. Execute the entry point of the specified assembly in a different thread. @@ -73,8 +70,7 @@ Assembly assembly TimeSpan? waitTimeout = null, bool stopApplication = true, Action? configureHostBuilder = null, - Action? entrypointCompleted = null - ) + Action? entrypointCompleted = null) { if (assembly.EntryPoint is null) return null; @@ -96,15 +92,13 @@ Assembly assembly return null; } - return args => - new HostingListener( - args, - assembly.EntryPoint, - waitTimeout ?? s_defaultWaitTimeout, - stopApplication, - configureHostBuilder, - entrypointCompleted - ).CreateHost(); + return args => new HostingListener( + args, + assembly.EntryPoint, + waitTimeout ?? s_defaultWaitTimeout, + stopApplication, + configureHostBuilder, + entrypointCompleted).CreateHost(); } private static Func? ResolveFactory(Assembly assembly, string name) @@ -130,8 +124,7 @@ private static bool IsFactory(MethodInfo? factory) => // Used by EF tooling without any Hosting references. Looses some return type safety checks. public static Func? ResolveServiceProviderFactory( Assembly assembly, - TimeSpan? waitTimeout = null - ) + TimeSpan? waitTimeout = null) { // Prefer the older patterns by default for back compat. var webHostFactory = ResolveWebHostFactory(assembly); @@ -168,10 +161,8 @@ static bool IsApplicationNameArg(string arg) => arg.Equals("--applicationName", StringComparison.OrdinalIgnoreCase) || arg.Equals("/applicationName", StringComparison.OrdinalIgnoreCase); - if ( - !args.Any(arg => IsApplicationNameArg(arg)) - && assembly.GetName().Name is string assemblyName - ) + if (!args.Any(arg => IsApplicationNameArg(arg)) + && assembly.GetName().Name is string assemblyName) args = args.Concat(new[] { "--applicationName", assemblyName }).ToArray(); var host = hostFactory(args); @@ -197,8 +188,7 @@ static bool IsApplicationNameArg(string arg) => } private sealed class HostingListener - : IObserver, - IObserver> + : IObserver, IObserver> { private static readonly AsyncLocal _currentListener = new(); private readonly string[] _args; @@ -217,8 +207,7 @@ public HostingListener( TimeSpan waitTimeout, bool stopApplication, Action? configure, - Action? entrypointCompleted - ) + Action? entrypointCompleted) { _args = args; _entryPoint = entryPoint; @@ -289,12 +278,10 @@ public object CreateHost() // build to throw _hostTcs.TrySetException( new InvalidOperationException( - "The entry point exited without ever building an IHost." - ) - ); + "The entry point exited without ever building an IHost.")); } - catch (TargetInvocationException tie) - when (tie.InnerException?.GetType().Name == "HostAbortedException") + catch (TargetInvocationException tie) when (tie.InnerException?.GetType().Name + == "HostAbortedException") { // The host was stopped by our own logic } @@ -331,8 +318,7 @@ public object CreateHost() // Wait before throwing an exception if (!_hostTcs.Task.Wait(_waitTimeout)) throw new InvalidOperationException( - $"Timed out waiting for the entry point to build the IHost after {s_defaultWaitTimeout}. This timeout can be modified using the '{TimeoutEnvironmentKey}' environment variable." - ); + $"Timed out waiting for the entry point to build the IHost after {s_defaultWaitTimeout}. This timeout can be modified using the '{TimeoutEnvironmentKey}' environment variable."); } catch (AggregateException) when (_hostTcs.Task.IsCompleted) { @@ -354,8 +340,7 @@ private void ThrowHostAborted() { var publicHostAbortedExceptionType = Type.GetType( "Microsoft.Extensions.Hosting.HostAbortedException, Microsoft.Extensions.Hosting.Abstractions", - false - ); + false); if (publicHostAbortedExceptionType != null) throw (Exception)Activator.CreateInstance(publicHostAbortedExceptionType)!; diff --git a/tests/AGENTS.md b/tests/AGENTS.md new file mode 100644 index 00000000..a813cef7 --- /dev/null +++ b/tests/AGENTS.md @@ -0,0 +1,78 @@ +# AGENTS.md (MinimalLambda tests) + +## Test stack + +- xUnit v3: `[Fact]`, `[Theory]`. +- Assertions: AwesomeAssertions `.Should()`. +- Mocking: NSubstitute. +- Test data: AutoFixture + AutoNSubstitute via `[AutoNSubstituteData]`. +- Prefer Arrange / Act / Assert comments. +- Keep tests simple and focused. Test class/method behavior, not dependencies. +- Do not write tests for behavior owned entirely by external library internals. + +## AutoNSubstituteData pattern + +Use `[Theory, AutoNSubstituteData]` for tests needing fixture data or mocks. +Use `[Fact]` for simple hardcoded cases. + +`[Frozen]` freezes generated instance so same mock is injected into system under test and available for assertions. + +```csharp +[Theory] +[AutoNSubstituteData] +internal async Task MyTest( + [Frozen] IMyInterface dependency, + MyClass instanceUnderTest +) +{ + // Act + await instanceUnderTest.DoSomething(); + + // Assert + await dependency.Received(1).ExpectedMethod(); +} +``` + +Prefer `[AutoNSubstituteData]` for simple dependency assertions. +Use manual `Fixture` helper class in test file when mocks need defaults or complex setup. + +## Commands + +```bash +task test:all +task test:verbose +task test:coverage +task test:watch +``` + +```bash +DOTNET_NOLOGO=1 dotnet test --configuration Release +DOTNET_NOLOGO=1 dotnet test --configuration Release -f net10.0 +DOTNET_NOLOGO=1 dotnet test \ + --project tests/MinimalLambda.UnitTests/MinimalLambda.UnitTests.csproj \ + --configuration Release -f net10.0 +``` + +## Single test commands + +Repo uses xUnit v3 on Microsoft.Testing.Platform. + +```bash +DOTNET_NOLOGO=1 dotnet test \ + --project tests/MinimalLambda.UnitTests/MinimalLambda.UnitTests.csproj \ + -f net10.0 -v q \ + --list-tests --no-progress --no-ansi + +DOTNET_NOLOGO=1 dotnet test \ + --project tests/MinimalLambda.UnitTests/MinimalLambda.UnitTests.csproj \ + -f net10.0 -v q \ + --filter-method "MyNamespace.MyTestClass.MyTestMethod" \ + --minimum-expected-tests 1 \ + --no-progress --no-ansi + +DOTNET_NOLOGO=1 dotnet test --project tests/MinimalLambda.UnitTests/MinimalLambda.UnitTests.csproj -f net10.0 -v q \ + --filter-class "MyNamespace.MyTestClass" --minimum-expected-tests 1 --no-progress --no-ansi + +DOTNET_NOLOGO=1 dotnet test --project tests/MinimalLambda.UnitTests/MinimalLambda.UnitTests.csproj -f net10.0 -v q \ + --filter-namespace "MyNamespace.Tests" --minimum-expected-tests 1 --no-progress --no-ansi +``` diff --git a/tests/CLAUDE.md b/tests/CLAUDE.md new file mode 120000 index 00000000..47dc3e3d --- /dev/null +++ b/tests/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/uv.lock b/uv.lock index 830ec114..e2afd369 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,94 @@ version = 1 revision = 3 requires-python = ">=3.13" +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "backrefs" +version = "7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/a7dd63622beef68cc0d3c3c36d472e143dd95443d5ebf14cd1a5b4dfbf11/backrefs-7.0.tar.gz", hash = "sha256:4989bb9e1e99eb23647c7160ed51fb21d0b41b5d200f2d3017da41e023097e82", size = 7012453, upload-time = "2026-04-28T16:28:04.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/39/39a31d7eae729ea14ed10c3ccef79371197177b9355a86cb3525709e8502/backrefs-7.0-py310-none-any.whl", hash = "sha256:b57cd227ea556b0aed3dc9b8da4628db4eabc0402c6d7fcfc69283a93955f7e9", size = 380824, upload-time = "2026-04-28T16:27:55.647Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b5/9302644225ba7dfa934a2ff2b9c7bb85701313a90dddb3dfaf693fa5bae2/backrefs-7.0-py311-none-any.whl", hash = "sha256:a0fa7360c63509e9e077e174ef4e6d3c21c8db94189b9d957289ae6d794b9475", size = 392626, upload-time = "2026-04-28T16:27:57.42Z" }, + { url = "https://files.pythonhosted.org/packages/36/da/87912ddec6e06feffbaa3d7aa18fc6352bee2e8f1fee185d7d1690f8f4e8/backrefs-7.0-py312-none-any.whl", hash = "sha256:ca42ce6a49ace3d75684dfa9937f3373902a63284ecb385ce36d15e5dcb41c12", size = 398537, upload-time = "2026-04-28T16:27:58.913Z" }, + { url = "https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl", hash = "sha256:f2c52955d631b9e1ac4cd56209f0a3a946d592b98e7790e77699339ae01c102a", size = 400491, upload-time = "2026-04-28T16:28:00.928Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5c/fb93d3092640a24dfb7bd7727a24016d7c01774ca013e60efd3f683c8002/backrefs-7.0-py314-none-any.whl", hash = "sha256:a6448b28180e3ca01134c9cf09dcebafad8531072e09903c5451748a05f24bc9", size = 412349, upload-time = "2026-04-28T16:28:02.412Z" }, +] + +[[package]] +name = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + [[package]] name = "click" version = "8.3.1" @@ -32,6 +120,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl", hash = "sha256:6de9ce507115cff0bed95ff0ce9ecc31088ef50cbdf09bc90a09349a318b3d00", size = 13475, upload-time = "2024-08-30T05:31:48.659Z" }, ] +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "idna" +version = "3.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, +] + +[[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, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + [[package]] name = "markdown" version = "3.10" @@ -41,6 +162,67 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678, upload-time = "2025-11-03T19:51:13.887Z" }, ] +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + [[package]] name = "minimal-lambda-docs" version = "0.1.0" @@ -48,13 +230,122 @@ source = { virtual = "." } [package.dev-dependencies] dev = [ + { name = "mkdocs-material" }, { name = "zensical" }, ] [package.metadata] [package.metadata.requires-dev] -dev = [{ name = "zensical", specifier = ">=0.0.33" }] +dev = [ + { name = "mkdocs-material", specifier = ">=9.7.6" }, + { name = "zensical", specifier = ">=0.0.40" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/29/6d2bcf41ae40802c4beda2432396fff97b8456fb496371d1bc7aad6512ec/mkdocs_material-9.7.6.tar.gz", hash = "sha256:00bdde50574f776d328b1862fe65daeaf581ec309bd150f7bff345a098c64a69", size = 4097959, upload-time = "2026-03-19T15:41:58.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl", hash = "sha256:71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba", size = 9305470, upload-time = "2026-03-19T15:41:55.217Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[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 = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] [[package]] name = "pygments" @@ -78,6 +369,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl", hash = "sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638", size = 268901, upload-time = "2026-03-29T15:01:53.244Z" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -114,30 +417,134 @@ wheels = [ { 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 = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + +[[package]] +name = "requests" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, +] + +[[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, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[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/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 = "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 = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + [[package]] name = "zensical" -version = "0.0.33" +version = "0.0.40" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "deepmerge" }, + { name = "jinja2" }, { name = "markdown" }, { name = "pygments" }, { name = "pymdown-extensions" }, { name = "pyyaml" }, + { name = "tomli" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/59/c2/dea4b86dc1ca2a7b55414017f12cfb12b5cfdf3a1ed7c77a04c271eb523b/zensical-0.0.33.tar.gz", hash = "sha256:05209cb4f80185c533e0d37c25d084ddc2050e3d5a4dd1b1812961c2ee0c3380", size = 3892278, upload-time = "2026-04-14T11:08:19.895Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/74/5f/45d5200405420a9d8ac91cf9e7826622ea12f3198e8e6ac4ffb481eb53bf/zensical-0.0.33-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f658e3c241cfbb560bd8811116a9486cff7e04d7d5aed73569dd533c74187450", size = 12416748, upload-time = "2026-04-14T11:07:43.246Z" }, - { url = "https://files.pythonhosted.org/packages/33/1e/aadaf31d6e4d20419ecedaf0b1c804e359ec23dcdb44c8d2bf6d8407080c/zensical-0.0.33-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:f9813ac3256c28e2e2f1ba5c9fab1b4bca62bbe0e0f8e85ac22d33b068b1b08a", size = 12293372, upload-time = "2026-04-14T11:07:46.569Z" }, - { url = "https://files.pythonhosted.org/packages/db/e5/838be8451ea8b2aecec39fbec3971060fc705e17f5741249740d9b6a6824/zensical-0.0.33-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3bad7ac71028769c5d1f3f84f448dbb7352db28d77095d1b40a8d1b0aa34ec30", size = 12659832, upload-time = "2026-04-14T11:07:50.754Z" }, - { url = "https://files.pythonhosted.org/packages/1e/5c/dd957d7c83efc13a70a6058d4190a3afcf29942aefb391120bca5466347d/zensical-0.0.33-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:06bb039daf044547c9400a52f9493b3cd486ba9baef3324fdcffd2e26e61105f", size = 12603847, upload-time = "2026-04-14T11:07:53.698Z" }, - { url = "https://files.pythonhosted.org/packages/b7/99/dd6ccc392ece1f34fb20ea339a01717badbbeb2fba1d4f3019a5028d0bcc/zensical-0.0.33-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:260238062b3139ece0edab93f4dbe7a12923453091f5aa580dfd73e799388076", size = 12956236, upload-time = "2026-04-14T11:07:56.728Z" }, - { url = "https://files.pythonhosted.org/packages/f4/76/e0a1b884eadf6afa7e2d56c90c268eec36836ac27e96ef250c0129e55417/zensical-0.0.33-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dff0f4afda7b8586bc4ab2a5684bce5b282232dd4e0cad3be4c73fedd264425", size = 12701944, upload-time = "2026-04-14T11:07:59.928Z" }, - { url = "https://files.pythonhosted.org/packages/38/38/e1ff13461e406864fa2b23fc828822659a7dbac5c79398f724d17f088540/zensical-0.0.33-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:207b4d81b208d75b97dc7bd318804550b886a3e852ef67429ef0e6b9442839d1", size = 12835444, upload-time = "2026-04-14T11:08:02.998Z" }, - { url = "https://files.pythonhosted.org/packages/41/04/7d24d52d6903fc5c511633afe8b5716fef19da09685327665cc127f61648/zensical-0.0.33-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:06d2f57f7bc8cc8fd904386020ea1365eebc411e8698a871e9525c885abca574", size = 12878419, upload-time = "2026-04-14T11:08:06.054Z" }, - { url = "https://files.pythonhosted.org/packages/9a/ec/87fc9e360c694ab006363c7834639eccafd0d26a487cd63dd609bd68f36a/zensical-0.0.33-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:c2851b82d83aa0b2ae4f8e99731cfeedeecebfa04e6b3fc4d375deca629fa240", size = 13022474, upload-time = "2026-04-14T11:08:09.007Z" }, - { url = "https://files.pythonhosted.org/packages/10/b3/0bf174ab6ceedb31d9af462073b5339c894b2084a27d42cb9f0906050d76/zensical-0.0.33-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:90daaf512b0429d7b9147ad5e6085b455d24803eff18b508aed738ca65444683", size = 12975233, upload-time = "2026-04-14T11:08:12.535Z" }, - { url = "https://files.pythonhosted.org/packages/a9/27/7cc3c2d284698647f60f3b823e0101e619c87edf158d47ee11bf4bfb6228/zensical-0.0.33-cp310-abi3-win32.whl", hash = "sha256:2701820597fe19361a12371129927c58c19633dcaa5f6986d610dce58cecd8c4", size = 12012664, upload-time = "2026-04-14T11:08:14.977Z" }, - { url = "https://files.pythonhosted.org/packages/25/0b/6be5c2fdaf9f1600577e7ba5e235d86b72a26f6af389efb146f978f76ac3/zensical-0.0.33-cp310-abi3-win_amd64.whl", hash = "sha256:a5a0911b4247708a55951b74c459f4d5faec5daaf287d23a2e1f0d96be1e647f", size = 12206255, upload-time = "2026-04-14T11:08:17.375Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/ba/a6/88062f7e235f58a5f05d82005fc35d9dbaed27c024fe9ffae5bce7f33661/zensical-0.0.40.tar.gz", hash = "sha256:5c294751977a664614cb84e987186ad8e282af77ce0d0d800fe48ee57791279d", size = 3920555, upload-time = "2026-05-04T16:19:07.962Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/c4/3066f4442923ca1e49269147b70ca7c84467524e8f5228724693b9ac85c2/zensical-0.0.40-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b65a7143c9c6a460880bf3e65b777952bd2dcede9dd17a6c6bac9b4a0686ad9b", size = 12691533, upload-time = "2026-05-04T16:18:31.72Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cb/03e961cbd01620ea91aeb835b0b4e8848c7bcdf5a799a620fb3e57bfc277/zensical-0.0.40-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:045bdcb6d00a11ddcab7d379d0d986cdf78dba8e9287d8e628ef11958241507d", size = 12556486, upload-time = "2026-05-04T16:18:35.278Z" }, + { url = "https://files.pythonhosted.org/packages/60/76/7dde50220808bdc5f5e63b97866a684418410b3cae9d00cdae1d449bcc20/zensical-0.0.40-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d48ec476c2e8ce3f8585a1278083aabc35ec80361f2c4fc4a53b9a525778f7fc", size = 12935602, upload-time = "2026-05-04T16:18:38.308Z" }, + { url = "https://files.pythonhosted.org/packages/51/55/6c8ef951c390b42249738f4338498e7a1fd64ff09e44d7cc19f5c948c45b/zensical-0.0.40-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:48c38e0ae314c25f2e5e64210bbad9be6e970f2d40fe9da106586ad90ce5e85e", size = 12904314, upload-time = "2026-05-04T16:18:41.007Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ae/95008f5dc2ee441efcdc2fab36ff29ce24d7477e53390fc340c8add39342/zensical-0.0.40-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f25f62dcd61f6306cab890dfa34c81d2709f5db290b4c3f2675343771db28c90", size = 13269946, upload-time = "2026-05-04T16:18:44.387Z" }, + { url = "https://files.pythonhosted.org/packages/b9/96/cdbb2bf04255ccaaa07861bdda1ee8dd1630d2233fc2f09636abbd5e084c/zensical-0.0.40-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:168fe3489dd93ae92978b4db11d9300c63e10d382b81634232c2872ce9e746c2", size = 12974962, upload-time = "2026-05-04T16:18:47.462Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ce/66e86f89fc15bbe667794ba67d7efc8fa72fe7a1be19e1efb4246ff55442/zensical-0.0.40-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8652ba203bd588ebf2d66bda4457a4a7d8e193c886960859c75081c0e3b946de", size = 13111599, upload-time = "2026-05-04T16:18:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/87/76/3d71ebdabb02d79a5c523b5e646141c362c9559947078c8d56a9f3bd7a30/zensical-0.0.40-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:9ffa6cf208b7ab6b771703be827d4d8c7f07f173abeffb35a8015a0b832b2a40", size = 13175406, upload-time = "2026-05-04T16:18:53.209Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6a/2bb5f730786d590f02cb0fef796c148d5ac0d5c1556f2d78c987ad4e1346/zensical-0.0.40-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:7101ba0c739c78bc3a57d22130b59b9e6fdf96c21c8a6b4244070de6b34527d4", size = 13324783, upload-time = "2026-05-04T16:18:56.41Z" }, + { url = "https://files.pythonhosted.org/packages/2f/8c/1d2ba1454360ee948dd0f0807b048c076d9578d0d9ebba2a438ecfa9f82f/zensical-0.0.40-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:39bf728a68a5418feeda8f3385cd1063fdb8d896a6812c3dede4267b2868df12", size = 13260045, upload-time = "2026-05-04T16:18:59.244Z" }, + { url = "https://files.pythonhosted.org/packages/6c/61/efd51c5c5e15cfd5498d59df250f60294cc44d36d8ce4dc2a76fa3669c2f/zensical-0.0.40-cp310-abi3-win32.whl", hash = "sha256:bc750c3ba8d11833d9b9ac8fc14adc3435225b6d17314a21a91eb60209511ca5", size = 12244913, upload-time = "2026-05-04T16:19:02.219Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9e/f3f2118fbcfd1c2dc705491c8864c596b1a748b67ffe2a024e512b9201ab/zensical-0.0.40-cp310-abi3-win_amd64.whl", hash = "sha256:c5c86ac468df2dfe515ff54ffa97725c38226f1e5c970059b7e88078abab89ab", size = 12475762, upload-time = "2026-05-04T16:19:05.025Z" }, ]