Refitter 0.8.1

There is a newer version of this package available.
See the version list below for details.
dotnet tool install --global Refitter --version 0.8.1                
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest # if you are setting up this repo
dotnet tool install --local Refitter --version 0.8.1                
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=Refitter&version=0.8.1                
nuke :add-package Refitter --version 0.8.1                

Refitter

Refitter is a CLI tool for generating a C# REST API Client using the Refit library. Refitter can generate the Refit interface from OpenAPI specifications

Installation:

The tool is packaged as a .NET Tool and is published to nuget.org. You can install the latest version of this tool like this:

dotnet tool install --global Refitter

Usage:

$ refitter --help
USAGE:
    refitter [URL or input file] [OPTIONS]

EXAMPLES:
    refitter ./openapi.json
    refitter https://petstore3.swagger.io/api/v3/openapi.yaml
    refitter ./openapi.json --settings-file ./openapi.refitter --output ./GeneratedCode.cs
    refitter ./openapi.json --namespace "Your.Namespace.Of.Choice.GeneratedCode" --output ./GeneratedCode.cs
    refitter ./openapi.json --namespace "Your.Namespace.Of.Choice.GeneratedCode" --internal
    refitter ./openapi.json --output ./IGeneratedCode.cs --interface-only
    refitter ./openapi.json --use-api-response
    refitter ./openapi.json --cancellation-tokens
    refitter ./openapi.json --no-operation-headers
    refitter ./openapi.json --no-accept-headers
    refitter ./openapi.json --use-iso-date-format
    refitter ./openapi.json --additional-namespace "Your.Additional.Namespace" --additional-namespace "Your.Other.Additional.Namespace"
    refitter ./openapi.json --multiple-interfaces ByEndpoint
    refitter ./openapi.json --tag Pet --tag Store --tag User
    refitter ./openapi.json --match-path '^/pet/.*'
    refitter ./openapi.json --no-deprecated-operations
    refitter ./openapi.json --optional-nullable-parameters

ARGUMENTS:
    [URL or input file]    URL or file path to OpenAPI Specification file

OPTIONS:
                                          DEFAULT                                                                                                                       
    -h, --help                                             Prints help information                                                                                      
    -s, --settings-file                                    Path to .refitter settings file. Specifying this will ignore all other settings (except for --output)        
    -n, --namespace                       GeneratedCode    Default namespace to use for generated types                                                                 
    -o, --output                          Output.cs        Path to Output file                                                                                          
        --no-auto-generated-header                         Don't add <auto-generated> header to output file                                                             
        --no-accept-headers                                Don't add <Accept> header to output file                                                                     
        --interface-only                                   Don't generate contract types                                                                                
        --use-api-response                                 Return Task<IApiResponse<T>> instead of Task<T>                                                              
        --internal                                         Set the accessibility of the generated types to 'internal'                                                   
        --cancellation-tokens                              Use cancellation tokens                                                                                      
        --no-operation-headers                             Don't generate operation headers                                                                             
        --no-logging                                       Don't log errors or collect telemetry                                                                        
        --additional-namespace                             Add additional namespace to generated types                                                                  
        --use-iso-date-format                              Explicitly format date query string parameters in ISO 8601 standard date format using delimiters (2023-06-15)
        --multiple-interfaces                              Generate a Refit interface for each endpoint. May be one of ByEndpoint, ByTag                                
        --match-path                                       Only include Paths that match the provided regular expression. May be set multiple times                     
        --tag                                              Only include Endpoints that contain this tag. May be set multiple times and result in OR'ed evaluation       
        --skip-validation                                  Skip validation of the OpenAPI specification                                                                 
        --no-deprecated-operations                         Don't generate deprecated operations                                                                         
        --operation-name-template                      	   Generate operation names using pattern like '{operationName}Async'                                           
	--optional-nullable-parameters                     Generate nullable parameters as optional parameters

To generate code from an OpenAPI specifications file, run the following:

$ refitter [path to OpenAPI spec file] --namespace "[Your.Namespace.Of.Choice.GeneratedCode]"

This will generate a file called Output.cs which contains the Refit interface and contract classes generated using NSwag

Using the generated code

Here's an example generated output from the Swagger Petstore example using the default settings

$ refitter ./openapi.json --namespace "Your.Namespace.Of.Choice.GeneratedCode"
using Refit;
using System.Threading.Tasks;
using System.Collections.Generic;

namespace Your.Namespace.Of.Choice.GeneratedCode
{
    public partial interface ISwaggerPetstore
    {
        /// <summary>
        /// Update an existing pet by Id
        /// </summary>
        [Put("/pet")]
        Task<Pet> UpdatePet([Body] Pet body);

        /// <summary>
        /// Add a new pet to the store
        /// </summary>
        [Post("/pet")]
        Task<Pet> AddPet([Body] Pet body);

        /// <summary>
        /// Multiple status values can be provided with comma separated strings
        /// </summary>
        [Get("/pet/findByStatus")]
        Task<ICollection<Pet>> FindPetsByStatus([Query] Status? status);

        /// <summary>
        /// Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.
        /// </summary>
        [Get("/pet/findByTags")]
        Task<ICollection<Pet>> FindPetsByTags([Query(CollectionFormat.Multi)] IEnumerable<string> tags);

        /// <summary>
        /// Returns a single pet
        /// </summary>
        [Get("/pet/{petId}")]
        Task<Pet> GetPetById(long petId);

        [Post("/pet/{petId}")]
        Task UpdatePetWithForm(long petId, [Query] string name, [Query] string status);

        [Delete("/pet/{petId}")]
        Task DeletePet(long petId, [Header("api_key")] string api_key);

        [Post("/pet/{petId}/uploadImage")]
        Task<ApiResponse> UploadFile(long petId, [Query] string additionalMetadata, StreamPart body);

        /// <summary>
        /// Returns a map of status codes to quantities
        /// </summary>
        [Get("/store/inventory")]
        Task<IDictionary<string, int>> GetInventory();

        /// <summary>
        /// Place a new order in the store
        /// </summary>
        [Post("/store/order")]
        Task<Order> PlaceOrder([Body] Order body);

        /// <summary>
        /// For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions
        /// </summary>
        [Get("/store/order/{orderId}")]
        Task<Order> GetOrderById(long orderId);

        /// <summary>
        /// For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors
        /// </summary>
        [Delete("/store/order/{orderId}")]
        Task DeleteOrder(long orderId);

        /// <summary>
        /// This can only be done by the logged in user.
        /// </summary>
        [Post("/user")]
        Task CreateUser([Body] User body);

        /// <summary>
        /// Creates list of users with given input array
        /// </summary>
        [Post("/user/createWithList")]
        Task<User> CreateUsersWithListInput([Body] IEnumerable<User> body);

        [Get("/user/login")]
        Task<string> LoginUser([Query] string username, [Query] string password);

        [Get("/user/logout")]
        Task LogoutUser();

        [Get("/user/{username}")]
        Task<User> GetUserByName(string username);

        /// <summary>
        /// This can only be done by the logged in user.
        /// </summary>
        [Put("/user/{username}")]
        Task UpdateUser(string username, [Body] User body);

        /// <summary>
        /// This can only be done by the logged in user.
        /// </summary>
        [Delete("/user/{username}")]
        Task DeleteUser(string username);
    }
}

Here's an example generated output from the Swagger Petstore example configured to wrap the return type in IApiResponse<T>

$ refitter ./openapi.json --namespace "Your.Namespace.Of.Choice.GeneratedCode" --use-api-response
using Refit;
using System.Threading.Tasks;
using System.Collections.Generic;

namespace Your.Namespace.Of.Choice.GeneratedCode.WithApiResponse
{
    public partial interface ISwaggerPetstore
    {
        /// <summary>
        /// Update an existing pet by Id
        /// </summary>
        [Put("/pet")]
        Task<IApiResponse<Pet>> UpdatePet([Body] Pet body);

        /// <summary>
        /// Add a new pet to the store
        /// </summary>
        [Post("/pet")]
        Task<IApiResponse<Pet>> AddPet([Body] Pet body);

        /// <summary>
        /// Multiple status values can be provided with comma separated strings
        /// </summary>
        [Get("/pet/findByStatus")]
        Task<IApiResponse<ICollection<Pet>>> FindPetsByStatus([Query] Status? status);

        /// <summary>
        /// Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.
        /// </summary>
        [System.Obsolete]
        [Get("/pet/findByTags")]
        Task<IApiResponse<ICollection<Pet>>> FindPetsByTags([Query(CollectionFormat.Multi)] IEnumerable<string> tags);

        /// <summary>
        /// Returns a single pet
        /// </summary>
        [Get("/pet/{petId}")]
        Task<IApiResponse<Pet>> GetPetById(long petId);

        [Post("/pet/{petId}")]
        Task UpdatePetWithForm(long petId, [Query] string name, [Query] string status);

        [Delete("/pet/{petId}")]
        Task DeletePet(long petId, [Header("api_key")] string api_key);

        [Post("/pet/{petId}/uploadImage")]
        Task<IApiResponse<ApiResponse>> UploadFile(long petId, [Query] string additionalMetadata, StreamPart body);

        /// <summary>
        /// Returns a map of status codes to quantities
        /// </summary>
        [Get("/store/inventory")]
        Task<IApiResponse<IDictionary<string, int>>> GetInventory();

        /// <summary>
        /// Place a new order in the store
        /// </summary>
        [Post("/store/order")]
        Task<IApiResponse<Order>> PlaceOrder([Body] Order body);

        /// <summary>
        /// For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions
        /// </summary>
        [Get("/store/order/{orderId}")]
        Task<IApiResponse<Order>> GetOrderById(long orderId);

        /// <summary>
        /// For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors
        /// </summary>
        [Delete("/store/order/{orderId}")]
        Task DeleteOrder(long orderId);

        /// <summary>
        /// This can only be done by the logged in user.
        /// </summary>
        [Post("/user")]
        Task CreateUser([Body] User body);

        /// <summary>
        /// Creates list of users with given input array
        /// </summary>
        [Post("/user/createWithList")]
        Task<IApiResponse<User>> CreateUsersWithListInput([Body] IEnumerable<User> body);

        [Get("/user/login")]
        Task<IApiResponse<string>> LoginUser([Query] string username, [Query] string password);

        [Get("/user/logout")]
        Task LogoutUser();

        [Get("/user/{username}")]
        Task<IApiResponse<User>> GetUserByName(string username);

        /// <summary>
        /// This can only be done by the logged in user.
        /// </summary>
        [Put("/user/{username}")]
        Task UpdateUser(string username, [Body] User body);

        /// <summary>
        /// This can only be done by the logged in user.
        /// </summary>
        [Delete("/user/{username}")]
        Task DeleteUser(string username);
    }
}

Here's an example generated output from the Swagger Petstore example configured to generate an interface for each endpoint

CLI Tool

$ refitter ./openapi.json --namespace "Your.Namespace.Of.Choice.GeneratedCode" --multiple-interfaces ByEndpoint

Output

/// <summary>
/// Update an existing pet
/// </summary>
public partial interface IUpdatePetEndpoint
{
    /// <summary>
    /// Update an existing pet by Id
    /// </summary>
    [Put("/pet")]
    Task<Pet> Execute([Body] Pet body);
}

/// <summary>
/// Add a new pet to the store
/// </summary>
public partial interface IAddPetEndpoint
{
    /// <summary>
    /// Add a new pet to the store
    /// </summary>
    [Post("/pet")]
    Task<Pet> Execute([Body] Pet body);
}

/// <summary>
/// Finds Pets by status
/// </summary>
public partial interface IFindPetsByStatusEndpoint
{
    /// <summary>
    /// Multiple status values can be provided with comma separated strings
    /// </summary>
    [Get("/pet/findByStatus")]
    Task<ICollection<Pet>> Execute([Query] Status? status);
}

/// <summary>
/// Finds Pets by tags
/// </summary>
public partial interface IFindPetsByTagsEndpoint
{
    /// <summary>
    /// Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.
    /// </summary>
    [Get("/pet/findByTags")]
    Task<ICollection<Pet>> Execute([Query(CollectionFormat.Multi)] IEnumerable<string> tags);
}

/// <summary>
/// Find pet by ID
/// </summary>
public partial interface IGetPetByIdEndpoint
{
    /// <summary>
    /// Returns a single pet
    /// </summary>
    [Get("/pet/{petId}")]
    Task<Pet> Execute(long petId);
}

/// <summary>
/// Updates a pet in the store with form data
/// </summary>
public partial interface IUpdatePetWithFormEndpoint
{
    [Post("/pet/{petId}")]
    Task Execute(long petId, [Query] string name, [Query] string status);
}

/// <summary>
/// Deletes a pet
/// </summary>
public partial interface IDeletePetEndpoint
{
    [Delete("/pet/{petId}")]
    Task Execute(long petId, [Header("api_key")] string api_key);
}

/// <summary>
/// uploads an image
/// </summary>
public partial interface IUploadFileEndpoint
{
    [Post("/pet/{petId}/uploadImage")]
    Task<ApiResponse> Execute(long petId, [Query] string additionalMetadata, StreamPart body);
}

/// <summary>
/// Returns pet inventories by status
/// </summary>
public partial interface IGetInventoryEndpoint
{
    /// <summary>
    /// Returns a map of status codes to quantities
    /// </summary>
    [Get("/store/inventory")]
    Task<IDictionary<string, int>> Execute();
}

/// <summary>
/// Place an order for a pet
/// </summary>
public partial interface IPlaceOrderEndpoint
{
    /// <summary>
    /// Place a new order in the store
    /// </summary>
    [Post("/store/order")]
    Task<Order> Execute([Body] Order body);
}

/// <summary>
/// Find purchase order by ID
/// </summary>
public partial interface IGetOrderByIdEndpoint
{
    /// <summary>
    /// For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions
    /// </summary>
    [Get("/store/order/{orderId}")]
    Task<Order> Execute(long orderId);
}

/// <summary>
/// Delete purchase order by ID
/// </summary>
public partial interface IDeleteOrderEndpoint
{
    /// <summary>
    /// For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors
    /// </summary>
    [Delete("/store/order/{orderId}")]
    Task Execute(long orderId);
}

/// <summary>
/// Create user
/// </summary>
public partial interface ICreateUserEndpoint
{
    /// <summary>
    /// This can only be done by the logged in user.
    /// </summary>
    [Post("/user")]
    Task Execute([Body] User body);
}

/// <summary>
/// Creates list of users with given input array
/// </summary>
public partial interface ICreateUsersWithListInputEndpoint
{
    /// <summary>
    /// Creates list of users with given input array
    /// </summary>
    [Post("/user/createWithList")]
    Task<User> Execute([Body] IEnumerable<User> body);
}

/// <summary>
/// Logs user into the system
/// </summary>
public partial interface ILoginUserEndpoint
{
    [Get("/user/login")]
    Task<string> Execute([Query] string username, [Query] string password);
}

/// <summary>
/// Logs out current logged in user session
/// </summary>
public partial interface ILogoutUserEndpoint
{
    [Get("/user/logout")]
    Task Execute();
}

/// <summary>
/// Get user by user name
/// </summary>
public partial interface IGetUserByNameEndpoint
{
    [Get("/user/{username}")]
    Task<User> Execute(string username);
}

/// <summary>
/// Update user
/// </summary>
public partial interface IUpdateUserEndpoint
{
    /// <summary>
    /// This can only be done by the logged in user.
    /// </summary>
    [Put("/user/{username}")]
    Task Execute(string username, [Body] User body);
}

/// <summary>
/// Delete user
/// </summary>
public partial interface IDeleteUserEndpoint
{
    /// <summary>
    /// This can only be done by the logged in user.
    /// </summary>
    [Delete("/user/{username}")]
    Task Execute(string username);
}

RestService

Here's an example usage of the generated code above

using Refit;
using System;
using System.Threading.Tasks;

namespace Your.Namespace.Of.Choice.GeneratedCode;

internal class Program
{
    private static async Task Main(string[] args)
    {
        var client = RestService.For<ISwaggerPetstore>("https://petstore3.swagger.io/api/v3");
        var pet = await client.GetPetById(1);

        Console.WriteLine("## Using Task<T> as return type ##");
        Console.WriteLine($"Name: {pet.Name}");
        Console.WriteLine($"Category: {pet.Category.Name}");
        Console.WriteLine($"Status: {pet.Status}");
        Console.WriteLine();

        var client2 = RestService.For<WithApiResponse.ISwaggerPetstore>("https://petstore3.swagger.io/api/v3");
        var response = await client2.GetPetById(2);

        Console.WriteLine("## Using Task<IApiResponse<T>> as return type ##");
        Console.WriteLine($"HTTP Status Code: {response.StatusCode}");
        Console.WriteLine($"Name: {response.Content.Name}");
        Console.WriteLine($"Category: {response.Content.Category.Name}");
        Console.WriteLine($"Status: {response.Content.Status}");
    }
}

The RestService class generates an implementation of ISwaggerPetstore that uses HttpClient to make its calls.

The code above when run will output something like this:

## Using Task<T> as return type ##
Name: Gatitotototo
Category: Chaucito
Status: Sold

## Using Task<IApiResponse<T>> as return type ##
HTTP Status Code: OK
Name: Gatitotototo
Category: Chaucito
Status: Sold

ASP.NET Core and HttpClientFactory

Here's an example Minimal API with the Refit.HttpClientFactory library:

using Refit;
using Your.Namespace.Of.Choice.GeneratedCode;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services
    .AddRefitClient<ISwaggerPetstore>()
    .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://petstore3.swagger.io/api/v3"));

var app = builder.Build();
app.MapGet(
        "/pet/{id:long}",
        async (ISwaggerPetstore petstore, long id) =>
        {
            try
            {
                return Results.Ok(await petstore.GetPetById(id));
            }
            catch (Refit.ApiException e)
            {
                return Results.StatusCode((int)e.StatusCode);
            }
        })
    .WithName("GetPetById")
    .WithOpenApi();

app.UseHttpsRedirection();
app.UseSwaggerUI();
app.UseSwagger();
app.Run();

.NET Core supports registering the generated ISwaggerPetstore interface via HttpClientFactory

The following request to the API above

$ curl -X 'GET' 'https://localhost:5001/pet/1' -H 'accept: application/json'

Returns a response that looks something like this:

{
  "id": 1,
  "name": "Special_char_owner_!@#$^&()`.testing",
  "photoUrls": [
    "https://petstore3.swagger.io/resources/photos/623389095.jpg"
  ],
  "tags": [],
  "status": "Sold"
}

System requirements

.NET 6.0 (LTS)

For tips and tricks on software development, check out my blog

If you find this useful and feel a bit generous then feel free to buy me a coffee ☕

Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

Version Downloads Last updated
1.4.1 55,042 11/20/2024
1.4.1-preview.62 150 11/4/2024
1.4.0 354,672 10/14/2024
1.4.0-preview.61 101 10/7/2024
1.3.2 325,928 9/23/2024
1.3.2-preview.60 48 9/23/2024
1.3.1 48,961 9/20/2024
1.3.0 96,805 9/14/2024
1.2.1-preview.59 60 9/13/2024
1.2.1-preview.58 71 9/11/2024
1.2.1-preview.57 54 9/11/2024
1.2.1-preview.56 68 9/9/2024
1.2.1-preview.55 3,228 9/2/2024
1.2.1-preview.54 4,710 8/29/2024
1.2.0 559,865 8/12/2024
1.2.0-preview.53 5,178 8/4/2024
1.2.0-preview.52 1,840 7/29/2024
1.1.3 359,831 7/19/2024
1.1.3-preview.51 50 7/19/2024
1.1.2 25,452 7/17/2024
1.1.2-preview.50 47 7/16/2024
1.1.2-preview.49 62 7/11/2024
1.1.1 188,752 7/6/2024
1.1.1-preview.48 67 7/4/2024
1.1.1-preview.47 62 7/1/2024
1.1.1-preview.46 59 6/28/2024
1.1.0.45-preview 98 6/25/2024
1.0.2 238,547 6/13/2024
1.0.1 48,339 6/7/2024
1.0.0 244,769 5/3/2024
0.9.9.44-preview 88 4/29/2024
0.9.9 8,093 3/7/2024
0.9.8 3,688 2/27/2024
0.9.7 71,988 2/7/2024
0.9.6 254 1/29/2024
0.9.5 38,843 1/15/2024
0.9.4.43-preview 122 1/15/2024
0.9.4 32,081 1/12/2024
0.9.3.42-preview 154 1/10/2024
0.9.2 20,112 1/10/2024
0.9.1 3,895 1/9/2024
0.9.0 3,098 1/9/2024
0.8.7.41-preview 158 1/3/2024
0.8.7.40-preview 134 12/20/2023
0.8.7 55,031 12/18/2023
0.8.6.39-preview 151 12/14/2023
0.8.6.38-preview 167 12/14/2023
0.8.6 3,544 12/11/2023
0.8.5 54,747 11/23/2023
0.8.4 390 11/7/2023
0.8.3 249 10/31/2023
0.8.2 436 10/9/2023
0.8.1 893 10/4/2023
0.8.0 1,746 9/23/2023
0.7.5 2,727 9/7/2023
0.7.4 347 9/6/2023
0.7.3.37-preview 217 8/25/2023
0.7.3.36-preview 239 8/25/2023
0.7.3.35-preview 253 8/21/2023
0.7.3.34-preview 255 8/15/2023
0.7.3.33-preview 236 8/12/2023
0.7.3 2,025 8/26/2023
0.7.2.32-preview 215 8/7/2023
0.7.2 3,819 8/7/2023
0.7.1.31-preview 250 8/2/2023
0.7.1.30-preview 186 8/2/2023
0.7.1.29-preview 252 8/1/2023
0.7.1 700 8/3/2023
0.7.0.28-preview 177 7/28/2023
0.7.0.27-preview 281 7/28/2023
0.7.0.26-preview 259 7/27/2023
0.7.0.23-preview 242 7/27/2023
0.7.0.22-preview 295 7/27/2023
0.7.0.21-preview 233 7/27/2023
0.7.0.20-preview 277 7/27/2023
0.7.0 418 7/31/2023
0.6.3 1,160 7/22/2023
0.6.2 12,159 6/22/2023
0.6.1 334 6/20/2023
0.6.0 707 6/15/2023
0.5.30 491 6/12/2023
0.5.29 147 6/12/2023
0.5.28 158 6/10/2023
0.5.27 213 5/24/2023
0.5.26 278 5/11/2023
0.5.25 197 5/10/2023
0.5.3 186 5/5/2023
0.5.2 151 5/2/2023
0.5.1 166 5/2/2023
0.5.0 211 4/28/2023
0.4.2 204 4/24/2023
0.4.1 430 4/3/2023
0.4.0 310 3/24/2023
0.3.17 7,427 3/24/2023
0.3.16 255 3/22/2023
0.3.4 215 3/22/2023
0.3.3 302 3/17/2023
0.3.2 269 3/16/2023
0.3.1 244 3/14/2023
0.3.0 236 3/14/2023
0.2.4-alpha 270 3/1/2023
0.2.3-alpha 179 2/27/2023
0.2.2-alpha 221 2/25/2023
0.2.1-alpha 247 2/25/2023
0.2.0-alpha 196 2/24/2023
0.1.5-alpha 251 2/18/2023
0.1.4-alpha 292 2/17/2023
0.1.3-alpha 272 2/17/2023
0.1.2-alpha 241 2/17/2023