mita

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 25, 2025 License: MIT Imports: 11 Imported by: 0

README

Mita

A minimum task scheduling library for Go, built on top of cron expressions with advanced features including concurrency control, error tracking, and flexible configuration options.

Features

  • 🕐 Flexible Scheduling - Standard cron expressions and fluent builder API
  • 🔒 Concurrency Control - Configurable max concurrent tasks and overlap prevention
  • 📊 Execution Tracking - Automatic statistics for runs, errors, and execution status
  • 🎯 Context Injection - Support for both static and dynamic context value injection
  • 🔄 Graceful Shutdown - Waits for running tasks to complete before shutting down
  • 🚀 Manual Triggers - Execute tasks on-demand outside their regular schedule
  • 🎛️ Task Management - Full CRUD operations: enable, disable, remove tasks
  • 📝 Logging - Customizable logging output with detailed execution info
  • 🌍 Timezone Support - Configure task execution timezone
  • Second Precision - Support for second-level scheduling granularity
  • 🛡️ Thread-Safe - Safe for concurrent use across multiple goroutines
  • 🔍 Rich Metadata - Track added time, last run, next run, and more
  • 🌐 Web Interface - Built-in web UI for monitoring and managing tasks

Installation

go get github.com/cymoo/mita

Quick Start

Basic Usage
package main

import (
    "context"
    "fmt"
    "time"
    "github.com/cymoo/mita"
)

func main() {
    // Create task manager
    tm := mita.New()
    
    // Add a task that runs every 5 seconds
    tm.AddTask("hello", mita.Every().Seconds(5), func(ctx context.Context) error {
        fmt.Println("Hello, World!")
        return nil
    })
    
    // Start the manager
    tm.Start()
    
    // Run for a while
    time.Sleep(30 * time.Second)
    
    // Graceful shutdown
    tm.Stop()
}
Advanced Configuration
// Create task manager with options
tm := mita.New(
    mita.WithLogger(customLogger),           // Custom logger
    mita.WithLocation(location),             // Set timezone
    mita.WithMaxConcurrent(5),               // Max concurrent tasks
    mita.WithAllowOverlapping(false),        // Prevent overlapping
    mita.WithContextValue("env", "prod"),    // Inject context values
)

Schedule Expressions

// Every second
mita.Every().Second()

// Every minute
mita.Every().Minute()

// Every hour
mita.Every().Hour()

// Every day
mita.Every().Day()

// Every N seconds
mita.Every().Seconds(30)

// Every N minutes
mita.Every().Minutes(15)

// Every N hours
mita.Every().Hours(6)

// Every N days
mita.Every().Days(2)

// Daily at specific time
mita.Every().Day().At(14, 30)  // 2:30 PM daily

// Specific weekday
mita.Every().Day().At(9, 0).OnWeekday(time.Monday)  // Monday 9:00 AM

// Specific day of month
mita.Every().Day().At(0, 0).OnDay(1)  // 1st of every month at midnight
Using Raw Cron Expressions
// Format: second minute hour day month weekday
mita.Cron("0 30 * * * *")     // Every hour at 30 minutes
mita.Cron("0 0 2 * * *")      // Daily at 2:00 AM
mita.Cron("0 */15 * * * *")   // Every 15 minutes
mita.Cron("0 0 0 1 * *")      // 1st of month at midnight
mita.Cron("0 0 9 * * 1")      // Every Monday at 9:00 AM

Configuration Options

WithLogger

Set a custom logger:

logger := log.New(os.Stdout, "[TASK] ", log.LstdFlags)
tm := mita.New(mita.WithLogger(logger))
WithLocation

Set timezone for task execution:

location, _ := time.LoadLocation("America/New_York")
tm := mita.New(mita.WithLocation(location))
WithMaxConcurrent

Limit maximum concurrent tasks (0 = unlimited):

tm := mita.New(mita.WithMaxConcurrent(3))
WithAllowOverlapping

Control whether the same task can run concurrently:

// Prevent same task from running multiple instances
tm := mita.New(mita.WithAllowOverlapping(false))

// Allow same task to run concurrently
tm := mita.New(mita.WithAllowOverlapping(true))
WithContextValue

Inject static context values available to all tasks:

tm := mita.New(
    mita.WithContextValue("database", dbConnection),
    mita.WithContextValue("cache", redisClient),
    mita.WithContextValue("env", "production"),
)
WithContextInjector

Dynamically inject context values per execution:

tm := mita.New(
    mita.WithContextInjector(func(ctx context.Context, taskName string) context.Context {
        // Generate unique ID for each execution
        ctx = context.WithValue(ctx, "request_id", uuid.New().String())
        ctx = context.WithValue(ctx, "timestamp", time.Now())
        return ctx
    }),
)

Task Management

Adding Tasks
err := tm.AddTask("backup", mita.Every().Day().At(2, 0), func(ctx context.Context) error {
    // Perform backup logic
    return nil
})
if err != nil {
    log.Fatal(err)
}
Manual Execution

Trigger a task immediately outside its schedule:

err := tm.RunTaskNow("backup")
if err != nil {
    log.Printf("Manual trigger failed: %v", err)
}
Disabling Tasks

Temporarily disable a task without removing it:

err := tm.DisableTask("backup")
Enabling Tasks

Re-enable a previously disabled task:

err := tm.EnableTask("backup")
Removing Tasks

Permanently remove a task:

err := tm.RemoveTask("backup")
Query Task Information

Get information about a specific task:

taskInfo, err := tm.GetTask("backup")
if err == nil {
    fmt.Printf("Task: %s\n", taskInfo.Name)
    fmt.Printf("Schedule: %s\n", taskInfo.Schedule)
    fmt.Printf("Run Count: %d\n", taskInfo.RunCount)
    fmt.Printf("Error Count: %d\n", taskInfo.ErrorCount)
    fmt.Printf("Last Run: %s\n", taskInfo.LastRun)
    fmt.Printf("Next Run: %s\n", taskInfo.NextRun)
    fmt.Printf("Enabled: %v\n", taskInfo.Enabled)
    fmt.Printf("Running: %v\n", taskInfo.Running)
}

List all tasks:

tasks := tm.ListTasks()
for _, task := range tasks {
    fmt.Printf("%s - Next: %s, Runs: %d, Errors: %d\n", 
        task.Name, task.NextRun, task.RunCount, task.ErrorCount)
}
Statistics

Get aggregated statistics:

stats := tm.GetStats()
fmt.Printf("Total Tasks: %v\n", stats["total_tasks"])
fmt.Printf("Enabled Tasks: %v\n", stats["enabled_tasks"])
fmt.Printf("Running Tasks: %v\n", stats["running_tasks"])
fmt.Printf("Total Runs: %v\n", stats["total_runs"])
fmt.Printf("Total Errors: %v\n", stats["total_errors"])
fmt.Printf("Max Concurrent: %v\n", stats["max_concurrent"])
fmt.Printf("Allow Overlapping: %v\n", stats["allow_overlapping"])

Working with Context

Retrieving Task Name
tm.AddTask("example", mita.Every().Minute(), func(ctx context.Context) error {
    taskName := mita.GetTaskName(ctx)
    fmt.Printf("Current task: %s\n", taskName)
    return nil
})
Accessing Injected Values
tm.AddTask("example", mita.Every().Minute(), func(ctx context.Context) error {
    // Get injected static context values
    db := ctx.Value(mita.CtxtKey("database")).(*sql.DB)
    requestID := ctx.Value(mita.CtxtKey("request_id")).(string)
    env := ctx.Value(mita.CtxtKey("env")).(string)
    
    // Use injected values
    log.Printf("[%s] Processing in %s environment", requestID, env)
    rows, err := db.Query("SELECT * FROM users")
    // ...
    return nil
})
Handling Context Cancellation

Always check for context cancellation in long-running tasks:

tm.AddTask("long-running", mita.Every().Hour(), func(ctx context.Context) error {
    for i := 0; i < 100; i++ {
        select {
        case <-ctx.Done():
            // Task manager is shutting down
            log.Println("Task cancelled, cleaning up...")
            return ctx.Err()
        default:
            // Continue work
            time.Sleep(time.Second)
            // Process item i
        }
    }
    return nil
})
Dynamic Context Updates

Update context values at runtime:

// Set or update a context value
tm.SetContextValue("feature_flag", true)

// Retrieve a context value
value := tm.GetContextValue("feature_flag")
if enabled, ok := value.(bool); ok && enabled {
    // Feature is enabled
}

Error Handling

Errors returned from task functions are automatically logged and tracked:

tm.AddTask("api-call", mita.Every().Minutes(5), func(ctx context.Context) error {
    resp, err := http.Get("https://api.example.com/data")
    if err != nil {
        return fmt.Errorf("API call failed: %w", err)
    }
    defer resp.Body.Close()
    
    if resp.StatusCode != 200 {
        return fmt.Errorf("API returned error status: %d", resp.StatusCode)
    }
    
    // Process response
    return nil
})

// Later, check for errors
taskInfo, _ := tm.GetTask("api-call")
if taskInfo.LastError != "" {
    log.Printf("Task last failed with: %s", taskInfo.LastError)
    log.Printf("Error rate: %d/%d (%.1f%%)", 
        taskInfo.ErrorCount, 
        taskInfo.RunCount,
        float64(taskInfo.ErrorCount)/float64(taskInfo.RunCount)*100)
}

Graceful Shutdown

The task manager supports graceful shutdown with proper cleanup:

func main() {
    tm := mita.New()
    // ... add tasks
    tm.Start()
    
    // Listen for system signals
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
    <-sigChan
    
    // Gracefully shutdown (waits up to 30 seconds for running tasks)
    tm.Stop()
}

The Stop() method:

  1. Cancels the manager context (stops new executions)
  2. Stops the cron scheduler
  3. Waits for all running tasks to complete (up to 30 seconds)
  4. Logs completion status

Web Management Interface

The task manager includes a built-in web interface for monitoring and managing tasks through your browser.

Starting the Web Server
package main

import (
    "net/http"
    "github.com/cymoo/mita"
)

func main() {
    tm := mita.New()
    
    // Add your tasks
    tm.AddTask("example", mita.Every().Minute(), func(ctx context.Context) error {
        // Task logic
        return nil
    })
    
    tm.Start()
    
    // Create web handler mounted at /tasks
    mux := tm.WebHandler("/tasks")
    
    // Start HTTP server
    http.ListenAndServe(":8080", mux)
}
Web Interface Features

The web interface provides:

  • 📋 Task List Page (/tasks/) - View all tasks with:

    • Real-time status (Enabled/Disabled/Running)
    • Execution statistics and success rates
    • Last run time and next scheduled run
    • Error information for failed tasks
    • Action buttons: Enable, Disable, Run Now, Remove
  • 📊 Statistics Page (/tasks/stats) - View aggregated metrics:

    • Total tasks, enabled tasks, running tasks
    • Total executions and error counts
    • Concurrency settings
    • Per-task performance with visual progress bars
Mounting at Custom Paths
// Mount at root
mux := tm.WebHandler("/")

// Mount at custom path
mux := tm.WebHandler("/admin/tasks")

// Integrate with existing HTTP server
existingMux := http.NewServeMux()
existingMux.Handle("/api/", apiHandler)
existingMux.Handle("/tasks/", tm.WebHandler("/tasks"))
Example with Authentication
func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Check authentication
        if !isAuthenticated(r) {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

func main() {
    tm := mita.New()
    tm.Start()
    
    mux := tm.WebHandler("/tasks")
    
    // Wrap with authentication
    http.ListenAndServe(":8080", authMiddleware(mux))
}
Integrating with Existing Applications
// Chi router
r := chi.NewRouter()
r.Mount("/", tm.WebHandler("/tasks"))

// Gin
router := gin.Default()
router.Any("/*any", gin.WrapH(tm.WebHandler("/tasks")))

// Echo
e := echo.New()
e.Any("/*", echo.WrapHandler(tm.WebHandler("/tasks")))

Complete Example

See _examples for a comprehensive, runnable example that demonstrates:

  • Multiple scheduling strategies
  • Concurrency control and overlap prevention
  • Error handling with simulated failures
  • Context injection (static and dynamic)
  • Task management operations (enable/disable)
  • Statistics and monitoring
  • Long-running tasks with cancellation
  • Graceful shutdown handling
  • Web interface integration

Run the example:

go run _examples/main.go

Best Practices

1. Set Appropriate Concurrency Limits
// For CPU-intensive tasks
tm := mita.New(mita.WithMaxConcurrent(runtime.NumCPU()))

// For I/O-bound tasks
tm := mita.New(mita.WithMaxConcurrent(20))
2. Handle Context Cancellation

Always respect context cancellation in long-running tasks:

func(ctx context.Context) error {
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
            // Do work
        }
    }
}
3. Return Meaningful Errors
func(ctx context.Context) error {
    if err := doWork(); err != nil {
        return fmt.Errorf("failed to process batch %d: %w", batchID, err)
    }
    return nil
}
4. Prevent Overlapping for Critical Tasks
tm := mita.New(mita.WithAllowOverlapping(false))
5. Monitor Task Health
// Periodically check task statistics
ticker := time.NewTicker(5 * time.Minute)
go func() {
    for range ticker.C {
        stats := tm.GetStats()
        errorRate := float64(stats["total_errors"].(int64)) / float64(stats["total_runs"].(int64))
        if errorRate > 0.1 { // More than 10% errors
            alert("High task error rate detected")
        }
    }
}()
6. Use Timeouts for External Calls
tm.AddTask("api-call", schedule, func(ctx context.Context) error {
    ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
    defer cancel()
    
    req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
    resp, err := client.Do(req)
    // ...
})
7. Clean Up Resources
tm.AddTask("db-task", schedule, func(ctx context.Context) error {
    conn := pool.Get()
    defer conn.Close()
    
    // Use connection
    return nil
})

Performance Considerations

  • Memory Usage: Each task stores minimal metadata (~200 bytes)
  • Goroutines: One goroutine per concurrent task execution
  • Lock Contention: Read-write locks minimize contention on task metadata
  • Cron Performance: Uses the highly optimized robfig/cron library

Thread Safety

All mita methods are thread-safe and can be called concurrently:

// Safe to call from multiple goroutines
go tm.AddTask(name1, schedule1, task1)
go tm.AddTask(name2, schedule2, task2)
go tm.RunTaskNow(name1)
go tm.GetStats()

Limitations

  • Maximum timeout for graceful shutdown: 30 seconds
  • Task names must be unique
  • Cron expressions use 6 fields (seconds supported)
  • Context values are copied, not referenced (use pointers for shared state)

FAQ

Q: Can I update a task's schedule without removing it?
A: Currently, you need to remove and re-add the task. A future version may support schedule updates.

Q: What happens if a task is already running when triggered manually?
A: If AllowOverlapping is false, you'll get an error. If true, both instances will run.

Q: How do I handle tasks that might run longer than their interval?
A: Set WithAllowOverlapping(false) to skip executions if the previous one is still running.

Q: Can I pause the entire task manager?
A: Not directly. You can disable all tasks individually or stop and restart the manager.

Q: Is it safe to modify context values during execution?
A: Use SetContextValue() to update values. Changes apply to new executions, not running ones.

Q: Can I customize the web interface? A: The web interface is embedded in the library. For customization, you can build your own interface using its API methods.

Q: Is the web interface secure? A: The web interface has no built-in authentication. Always add authentication middleware when exposing it publicly (see examples above).

Testing

To test your tasks:

func TestMyTask(t *testing.T) {
    tm := mita.New()
    
    executed := false
    tm.AddTask("test", mita.Every().Second(), func(ctx context.Context) error {
        executed = true
        return nil
    })
    
    tm.Start()
    time.Sleep(2 * time.Second)
    tm.Stop()
    
    if !executed {
        t.Error("Task was not executed")
    }
}

License

MIT License - see LICENSE file for details.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func GetTaskName

func GetTaskName(ctx context.Context) string

GetTaskName extracts the task name from a task context. Returns empty string if the context doesn't contain a task name.

Types

type CronSchedule

type CronSchedule struct {
	// contains filtered or unexported fields
}

CronSchedule wraps a standard cron expression string.

func Cron

func Cron(expr string) *CronSchedule

Cron creates a Schedule from a cron expression string. The expression should be in the format: "second minute hour day month weekday"

func (*CronSchedule) String

func (c *CronSchedule) String() string

String returns the cron expression.

type CtxtKey

type CtxtKey string

CtxtKey is a custom type for context keys to avoid collisions.

type Option

type Option func(*TaskManager)

Option is a functional option for configuring TaskManager.

func WithAllowOverlapping

func WithAllowOverlapping(allow bool) Option

WithAllowOverlapping controls whether the same task can run multiple instances concurrently. By default, overlapping is not allowed (false).

func WithContextInjector

func WithContextInjector(injector func(ctx context.Context, taskName string) context.Context) Option

WithContextInjector sets a custom function to dynamically inject values into task contexts. The injector is called for each task execution and receives the base context and task name.

func WithContextValue

func WithContextValue(key string, value any) Option

WithContextValue adds a static key-value pair that will be injected into all task contexts.

func WithLocation

func WithLocation(loc *time.Location) Option

WithLocation sets the timezone for cron schedule interpretation.

func WithLogger

func WithLogger(logger *log.Logger) Option

WithLogger sets a custom logger for the task manager. If not provided, log.Default() will be used.

func WithMaxConcurrent

func WithMaxConcurrent(max int) Option

WithMaxConcurrent sets the maximum number of tasks that can run concurrently. A value of 0 means unlimited concurrency. Negative values are treated as 0.

type Schedule

type Schedule interface {
	String() string
}

Schedule represents a task scheduling expression.

type ScheduleBuilder

type ScheduleBuilder struct {
	// contains filtered or unexported fields
}

ScheduleBuilder provides a fluent API for building cron schedules.

func Every

func Every() *ScheduleBuilder

Every creates a new ScheduleBuilder with default values (runs every minute).

func (*ScheduleBuilder) At

func (s *ScheduleBuilder) At(hour, minute int) *ScheduleBuilder

At specifies a specific time of day for the schedule. For example, At(14, 30) runs at 2:30 PM. Hour must be 0-23 and minute must be 0-59, otherwise the method panics.

func (*ScheduleBuilder) Day

func (s *ScheduleBuilder) Day() *ScheduleBuilder

Day configures the schedule to run every day (at midnight).

func (*ScheduleBuilder) Days

func (s *ScheduleBuilder) Days(interval int) *ScheduleBuilder

Days configures the schedule to run at the specified day interval. For example, Days(2) runs every 2 days at midnight. Interval must be positive, otherwise the method panics.

func (*ScheduleBuilder) Hour

func (s *ScheduleBuilder) Hour() *ScheduleBuilder

Hour configures the schedule to run every hour (at 0 minutes, 0 seconds).

func (*ScheduleBuilder) Hours

func (s *ScheduleBuilder) Hours(interval int) *ScheduleBuilder

Hours configures the schedule to run at the specified hour interval. For example, Hours(6) runs every 6 hours. Interval must be positive, otherwise the method panics.

func (*ScheduleBuilder) Minute

func (s *ScheduleBuilder) Minute() *ScheduleBuilder

Minute configures the schedule to run every minute (at 0 seconds).

func (*ScheduleBuilder) Minutes

func (s *ScheduleBuilder) Minutes(interval int) *ScheduleBuilder

Minutes configures the schedule to run at the specified minute interval. For example, Minutes(15) runs every 15 minutes. Interval must be positive, otherwise the method panics.

func (*ScheduleBuilder) OnDay

func (s *ScheduleBuilder) OnDay(day int) *ScheduleBuilder

OnDay restricts the schedule to a specific day of the month. For example, OnDay(15) runs on the 15th of each month. Day must be between 1 and 31, otherwise the method panics.

func (*ScheduleBuilder) OnWeekday

func (s *ScheduleBuilder) OnWeekday(weekday time.Weekday) *ScheduleBuilder

OnWeekday restricts the schedule to a specific day of the week. For example, OnWeekday(time.Monday) runs only on Mondays.

func (*ScheduleBuilder) Second

func (s *ScheduleBuilder) Second() *ScheduleBuilder

Second configures the schedule to run every second.

func (*ScheduleBuilder) Seconds

func (s *ScheduleBuilder) Seconds(interval int) *ScheduleBuilder

Seconds configures the schedule to run at the specified second interval. For example, Seconds(30) runs every 30 seconds. Interval must be positive, otherwise the method panics.

func (*ScheduleBuilder) String

func (s *ScheduleBuilder) String() string

String converts the builder to a cron expression string.

type Task

type Task func(ctx context.Context) error

Task represents a function that performs work within a given context. It should return an error if the task execution fails.

type TaskInfo

type TaskInfo struct {
	Name       string       // Unique identifier for the task
	Schedule   string       // Cron expression for the task schedule
	Task       Task         // The actual task function to execute
	EntryID    cron.EntryID // Cron entry ID for this task
	AddedAt    time.Time    // When the task was added to the manager
	LastRun    time.Time    // Last execution time
	NextRun    time.Time    // Next scheduled execution time
	RunCount   int64        // Total number of executions
	ErrorCount int64        // Total number of failed executions
	LastError  string       // Most recent error message (empty if last run succeeded)
	Enabled    bool         // Whether the task is enabled for execution
	Running    bool         // Whether the task is currently executing
}

TaskInfo holds metadata and statistics about a scheduled task.

type TaskManager

type TaskManager struct {
	// contains filtered or unexported fields
}

TaskManager orchestrates scheduled task execution with concurrent control, error tracking, and flexible configuration options.

func New

func New(opts ...Option) *TaskManager

New creates a new TaskManager with the given options. The manager must be started with Start() before tasks will execute.

func (*TaskManager) AddTask

func (tm *TaskManager) AddTask(name string, schedule Schedule, task Task) error

AddTask registers a new task with the given name and schedule. Returns an error if a task with the same name already exists or if the schedule is invalid.

func (*TaskManager) DisableTask

func (tm *TaskManager) DisableTask(name string) error

DisableTask disables a task without removing it. The task will not execute but can be re-enabled later.

func (*TaskManager) EnableTask

func (tm *TaskManager) EnableTask(name string) error

EnableTask enables a previously disabled task. The task will resume executing on its schedule.

func (*TaskManager) GetContextValue

func (tm *TaskManager) GetContextValue(key string) any

GetContextValue retrieves a static context value. Returns nil if the key does not exist.

func (*TaskManager) GetStats

func (tm *TaskManager) GetStats() map[string]interface{}

GetStats returns aggregated statistics about the task manager and all tasks.

func (*TaskManager) GetTask

func (tm *TaskManager) GetTask(name string) (*TaskInfo, error)

GetTask returns a copy of the task information for the given task name. Returns an error if the task does not exist.

func (*TaskManager) IsRunning

func (tm *TaskManager) IsRunning() bool

IsRunning checks whether the task manager is currently running.

func (*TaskManager) ListTasks

func (tm *TaskManager) ListTasks() []*TaskInfo

ListTasks returns a copy of all task information. The returned slice can be safely modified without affecting the manager.

func (*TaskManager) RemoveTask

func (tm *TaskManager) RemoveTask(name string) error

RemoveTask removes a task from the manager. Returns an error if the task does not exist.

func (*TaskManager) RunTaskNow

func (tm *TaskManager) RunTaskNow(name string) error

RunTaskNow immediately executes a task outside of its regular schedule. The execution is asynchronous and subject to the same concurrency limits and overlap rules as scheduled executions.

func (*TaskManager) SetContextValue

func (tm *TaskManager) SetContextValue(key string, value any)

SetContextValue adds or updates a static context value that will be injected into all task contexts.

func (*TaskManager) Start

func (tm *TaskManager) Start()

Start begins the task scheduler. Tasks will start executing according to their schedules.

func (*TaskManager) Stop

func (tm *TaskManager) Stop()

Stop gracefully shuts down the task manager. It stops accepting new task executions and waits for running tasks to complete or times out after 30 seconds.

func (*TaskManager) WebHandler

func (tm *TaskManager) WebHandler(baseURL string) *http.ServeMux

WebHandler creates an HTTP handler for the task manager web interface. The baseURL parameter should be the URL prefix where the handler is mounted (e.g., "/tasks"). Returns a ServeMux that can be integrated into your HTTP server.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL