├── bin ├── hermit.hcl ├── go ├── gofmt ├── .go-1.19.4.pkg ├── .rustup-1.25.1.pkg ├── cargo ├── rls ├── rustc ├── rustup ├── cargo-fmt ├── cargo-miri ├── rust-gdb ├── rust-gdbgui ├── rust-lldb ├── rustdoc ├── rustfmt ├── cargo-clippy ├── clippy-driver ├── README.hermit.md ├── activate-hermit └── hermit ├── go.mod ├── .gitignore ├── rust-toolchain.toml ├── setup.cfg ├── examples-rs ├── src │ └── bin │ │ ├── ok.rs │ │ └── signal.rs ├── Cargo.toml └── Cargo.lock ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── signal_unix_test.go ├── pyproject.toml ├── signal_unix.go ├── Cargo.toml ├── test_readme.py ├── exit_test.go ├── readme_test.go ├── docs └── sysexits.md ├── README.md ├── exit.py ├── exit.go ├── src └── lib.rs ├── Cargo.lock ├── LICENSE └── poetry.lock /bin/hermit.hcl: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bin/go: -------------------------------------------------------------------------------- 1 | .go-1.19.4.pkg -------------------------------------------------------------------------------- /bin/gofmt: -------------------------------------------------------------------------------- 1 | .go-1.19.4.pkg -------------------------------------------------------------------------------- /bin/.go-1.19.4.pkg: -------------------------------------------------------------------------------- 1 | hermit -------------------------------------------------------------------------------- /bin/.rustup-1.25.1.pkg: -------------------------------------------------------------------------------- 1 | hermit -------------------------------------------------------------------------------- /bin/cargo: -------------------------------------------------------------------------------- 1 | .rustup-1.25.1.pkg -------------------------------------------------------------------------------- /bin/rls: -------------------------------------------------------------------------------- 1 | .rustup-1.25.1.pkg -------------------------------------------------------------------------------- /bin/rustc: -------------------------------------------------------------------------------- 1 | .rustup-1.25.1.pkg -------------------------------------------------------------------------------- /bin/rustup: -------------------------------------------------------------------------------- 1 | .rustup-1.25.1.pkg -------------------------------------------------------------------------------- /bin/cargo-fmt: -------------------------------------------------------------------------------- 1 | .rustup-1.25.1.pkg -------------------------------------------------------------------------------- /bin/cargo-miri: -------------------------------------------------------------------------------- 1 | .rustup-1.25.1.pkg -------------------------------------------------------------------------------- /bin/rust-gdb: -------------------------------------------------------------------------------- 1 | .rustup-1.25.1.pkg -------------------------------------------------------------------------------- /bin/rust-gdbgui: -------------------------------------------------------------------------------- 1 | .rustup-1.25.1.pkg -------------------------------------------------------------------------------- /bin/rust-lldb: -------------------------------------------------------------------------------- 1 | .rustup-1.25.1.pkg -------------------------------------------------------------------------------- /bin/rustdoc: -------------------------------------------------------------------------------- 1 | .rustup-1.25.1.pkg -------------------------------------------------------------------------------- /bin/rustfmt: -------------------------------------------------------------------------------- 1 | .rustup-1.25.1.pkg -------------------------------------------------------------------------------- /bin/cargo-clippy: -------------------------------------------------------------------------------- 1 | .rustup-1.25.1.pkg -------------------------------------------------------------------------------- /bin/clippy-driver: -------------------------------------------------------------------------------- 1 | .rustup-1.25.1.pkg -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/square/exit 2 | 3 | go 1.19 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.hermit 2 | 3 | /target 4 | __pycache__ 5 | *.pyc 6 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | profile = "default" 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 100 3 | extend-ignore = E203,W503,F821 4 | exclude = docs 5 | -------------------------------------------------------------------------------- /examples-rs/src/bin/ok.rs: -------------------------------------------------------------------------------- 1 | use semantic_exit::{exit, Code::OK}; 2 | 3 | fn main() { 4 | exit(OK); 5 | } 6 | -------------------------------------------------------------------------------- /examples-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "examples-rs" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [[bin]] 7 | name = "ok" 8 | 9 | [dependencies] 10 | semantic-exit = { path = "../" } 11 | signal-hook = "0.3.14" 12 | -------------------------------------------------------------------------------- /bin/README.hermit.md: -------------------------------------------------------------------------------- 1 | # Hermit environment 2 | 3 | This is a [Hermit](https://github.com/cashapp/hermit) bin directory. 4 | 5 | The symlinks in this directory are managed by Hermit and will automatically 6 | download and install Hermit itself as well as packages. These packages are 7 | local to this environment. 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | 8 | - package-ecosystem: github-actions 9 | directory: / 10 | schedule: 11 | interval: weekly 12 | 13 | - package-ecosystem: gomod 14 | directory: / 15 | schedule: 16 | interval: weekly 17 | -------------------------------------------------------------------------------- /signal_unix_test.go: -------------------------------------------------------------------------------- 1 | package exit 2 | 3 | import ( 4 | "syscall" 5 | "testing" 6 | ) 7 | 8 | func TestFromSignal(t *testing.T) { 9 | scenarios := []struct { 10 | signal syscall.Signal 11 | expected Code 12 | }{ 13 | {syscall.SIGINT, Code(130)}, 14 | } 15 | 16 | for _, s := range scenarios { 17 | if got := FromSignal(s.signal); got != s.expected { 18 | t.Errorf("Expected FromSignal(%d) to be %d; got %d", s.signal, s.expected, got) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "exit" 3 | version = "0.1.0" 4 | description = "Semantic exit codes for CLI tools." 5 | authors = ["John Weachock "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.8.1" 9 | 10 | [tool.poetry.dev-dependencies] 11 | pytest = "^7.2.0" 12 | isort = "^5.11.4" 13 | flake8 = "^6.0.0" 14 | black = "^24.3.0" 15 | 16 | [build-system] 17 | requires = ["poetry-core>=1.0.0"] 18 | build-backend = "poetry.core.masonry.api" 19 | -------------------------------------------------------------------------------- /signal_unix.go: -------------------------------------------------------------------------------- 1 | //go:build linux || darwin 2 | // +build linux darwin 3 | 4 | package exit 5 | 6 | import "syscall" 7 | 8 | // FromSignal returns the exit code that corresponds to when a program 9 | // exits in response to a signal. 10 | func FromSignal(signal syscall.Signal) Code { 11 | // According to https://tldp.org/LDP/abs/html/exitcodes.html, it's standard 12 | // for a unix process to exit with 128 + n where n is a fatal signal. 13 | return Code(128 + int(signal)) 14 | } 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "semantic-exit" 3 | description = "Semantic exit codes inspired by HTTP status codes" 4 | authors = ["Bob Lail ", "Scott Robinson "] 5 | license = "Apache-2.0" 6 | repository = "https://github.com/square/exit" 7 | readme = "README.md" 8 | version = "1.0.0" 9 | edition = "2021" 10 | 11 | [dependencies] 12 | num-traits = "0.2" 13 | num-derive = "0.4" 14 | thiserror = "2.0.17" 15 | 16 | [dev-dependencies] 17 | libc = "0.2" 18 | regex = "1.12.2" 19 | test-case = "3.3.1" 20 | -------------------------------------------------------------------------------- /examples-rs/src/bin/signal.rs: -------------------------------------------------------------------------------- 1 | use semantic_exit::from_signal; 2 | use signal_hook::{consts::SIGINT, iterator::Signals}; 3 | use std::process::exit; 4 | use std::{error::Error, thread, time::Duration}; 5 | 6 | fn main() -> Result<(), Box> { 7 | let mut signals = Signals::new([SIGINT])?; 8 | 9 | thread::spawn(move || { 10 | for sig in signals.forever() { 11 | println!("Received signal {:?}", sig); 12 | exit(from_signal(sig)); 13 | } 14 | }); 15 | 16 | thread::sleep(Duration::from_secs(2)); 17 | 18 | Ok(()) 19 | } 20 | -------------------------------------------------------------------------------- /bin/activate-hermit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This file must be used with "source bin/activate-hermit" from bash or zsh. 3 | # You cannot run it directly 4 | # 5 | # THIS FILE IS GENERATED; DO NOT MODIFY 6 | 7 | if [ "${BASH_SOURCE-}" = "$0" ]; then 8 | echo "You must source this script: \$ source $0" >&2 9 | exit 33 10 | fi 11 | 12 | BIN_DIR="$(dirname "${BASH_SOURCE[0]:-${(%):-%x}}")" 13 | if "${BIN_DIR}/hermit" noop > /dev/null; then 14 | eval "$("${BIN_DIR}/hermit" activate "${BIN_DIR}/..")" 15 | 16 | if [ -n "${BASH-}" ] || [ -n "${ZSH_VERSION-}" ]; then 17 | hash -r 2>/dev/null 18 | fi 19 | 20 | echo "Hermit environment $("${HERMIT_ENV}"/bin/hermit env HERMIT_ENV) activated" 21 | fi 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | name: CI 4 | jobs: 5 | go: 6 | name: Test (Go) 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v5 10 | 11 | - name: Set up Go 12 | uses: actions/setup-go@v6 13 | with: 14 | go-version: 1.19 15 | 16 | - name: Test 17 | run: go test -v 18 | 19 | rust: 20 | name: Test (Rust) 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v5 24 | 25 | - run: bin/cargo test --all-features --all-targets --locked 26 | 27 | python: 28 | name: Test (Python) 29 | runs-on: ubuntu-latest 30 | strategy: 31 | matrix: 32 | python-version: ["3.8", "3.9", "3.10", "3.11"] 33 | 34 | steps: 35 | - uses: actions/checkout@v5 36 | 37 | - name: Set up Python ${{ matrix.python-version }} 38 | uses: actions/setup-python@v6 39 | with: 40 | python-version: ${{ matrix.python-version }} 41 | 42 | - name: Install dependencies 43 | run: | 44 | python -m pip install --upgrade pip 45 | pip install pytest 46 | 47 | - name: Test 48 | run: | 49 | pytest 50 | -------------------------------------------------------------------------------- /test_readme.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import pytest 4 | 5 | import exit 6 | 7 | with open("README.md") as fp: 8 | pattern = re.compile("\\| (\\d+) \\| `(\\w+)` \\| .* \\|") 9 | expected_constants = {} 10 | for line in fp: 11 | if result := pattern.match(line): 12 | expected_constants[result.group(2)] = int(result.group(1)) 13 | 14 | actual_constants = {code.name: code.value for code in exit.Code} 15 | 16 | 17 | @pytest.mark.parametrize("code_name", set(expected_constants) | set(actual_constants)) 18 | def test_foo(code_name): 19 | if code_name not in actual_constants: 20 | assert ( 21 | False 22 | ), f"exit.py does not define the exit code {code_name} ({expected_constants[code_name]})" 23 | 24 | if code_name not in expected_constants: 25 | assert ( 26 | False 27 | ), f"exit.py defines an undocumented exit code {code_name} ({actual_constants[code_name]})" 28 | 29 | if actual_constants[code_name] != expected_constants[code_name]: 30 | assert False, ( 31 | f"exit.py defines {code_name} as {actual_constants[code_name]}," 32 | f"README.md defines it as {expected_constants[code_name]}" 33 | ) 34 | -------------------------------------------------------------------------------- /exit_test.go: -------------------------------------------------------------------------------- 1 | package exit 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "testing" 7 | ) 8 | 9 | func TestFromError(t *testing.T) { 10 | // Test cases for the FromError function 11 | tests := []struct { 12 | name string 13 | err error 14 | want int 15 | }{ 16 | { 17 | name: "nil", 18 | err: nil, 19 | want: 0, 20 | }, 21 | { 22 | name: "exit error", 23 | err: ErrInternalError, 24 | want: 100, 25 | }, 26 | { 27 | name: "error", 28 | err: fmt.Errorf("wrapped error"), 29 | want: 1, 30 | }, 31 | { 32 | name: "wrapped error", 33 | err: Wrap(fmt.Errorf("wrapped error"), UsageError), 34 | want: 80, 35 | }, 36 | { 37 | name: "error wrapped more than once", 38 | err: Wrap(Wrap(fmt.Errorf("wrapped error"), UsageError), Unavailable), 39 | want: 101, 40 | }, 41 | { 42 | name: "*exec.ExitError", 43 | err: exec.Command("sh", "-c", "exit 3").Run(), 44 | want: 3, 45 | }, 46 | { 47 | name: "wrapped *exec.ExitError", 48 | err: Wrap(exec.Command("exit 3").Run(), NotOK), 49 | want: 1, 50 | }, 51 | } 52 | 53 | for _, tt := range tests { 54 | t.Run(tt.name, func(t *testing.T) { 55 | if got := FromError(tt.err); got != tt.want { 56 | t.Errorf("FromError(%+v): got %v, want %v", tt.err, got, tt.want) 57 | } 58 | }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /bin/hermit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # THIS FILE IS GENERATED; DO NOT MODIFY 4 | 5 | set -eo pipefail 6 | 7 | export HERMIT_USER_HOME=~ 8 | 9 | if [ -z "${HERMIT_STATE_DIR}" ]; then 10 | case "$(uname -s)" in 11 | Darwin) 12 | export HERMIT_STATE_DIR="${HERMIT_USER_HOME}/Library/Caches/hermit" 13 | ;; 14 | Linux) 15 | export HERMIT_STATE_DIR="${XDG_CACHE_HOME:-${HERMIT_USER_HOME}/.cache}/hermit" 16 | ;; 17 | esac 18 | fi 19 | 20 | export HERMIT_DIST_URL="${HERMIT_DIST_URL:-https://github.com/cashapp/hermit/releases/download/stable}" 21 | HERMIT_CHANNEL="$(basename "${HERMIT_DIST_URL}")" 22 | export HERMIT_CHANNEL 23 | export HERMIT_EXE=${HERMIT_EXE:-${HERMIT_STATE_DIR}/pkg/hermit@${HERMIT_CHANNEL}/hermit} 24 | 25 | if [ ! -x "${HERMIT_EXE}" ]; then 26 | echo "Bootstrapping ${HERMIT_EXE} from ${HERMIT_DIST_URL}" 1>&2 27 | INSTALL_SCRIPT="$(mktemp)" 28 | # This value must match that of the install script 29 | INSTALL_SCRIPT_SHA256="180e997dd837f839a3072a5e2f558619b6d12555cd5452d3ab19d87720704e38" 30 | if [ "${INSTALL_SCRIPT_SHA256}" = "BYPASS" ]; then 31 | curl -fsSL "${HERMIT_DIST_URL}/install.sh" -o "${INSTALL_SCRIPT}" 32 | else 33 | # Install script is versioned by its sha256sum value 34 | curl -fsSL "${HERMIT_DIST_URL}/install-${INSTALL_SCRIPT_SHA256}.sh" -o "${INSTALL_SCRIPT}" 35 | # Verify install script's sha256sum 36 | openssl dgst -sha256 "${INSTALL_SCRIPT}" | \ 37 | awk -v EXPECTED="$INSTALL_SCRIPT_SHA256" \ 38 | '$2!=EXPECTED {print "Install script sha256 " $2 " does not match " EXPECTED; exit 1}' 39 | fi 40 | /bin/bash "${INSTALL_SCRIPT}" 1>&2 41 | fi 42 | 43 | exec "${HERMIT_EXE}" --level=fatal exec "$0" -- "$@" 44 | -------------------------------------------------------------------------------- /readme_test.go: -------------------------------------------------------------------------------- 1 | package exit 2 | 3 | import ( 4 | _ "embed" 5 | "go/ast" 6 | "go/importer" 7 | "go/parser" 8 | "go/token" 9 | "go/types" 10 | "regexp" 11 | "testing" 12 | ) 13 | 14 | //go:embed README.md 15 | var readme string 16 | 17 | //go:embed exit.go 18 | var src string 19 | 20 | func TestExitCodesMatchReadme(t *testing.T) { 21 | var f *ast.File 22 | var err error 23 | 24 | re := regexp.MustCompile("\\| (\\d+) \\| `(\\w+)` \\| .* \\|\n") 25 | expectedConstants := make(map[string]string) 26 | for _, submatch := range re.FindAllStringSubmatch(readme, -1) { 27 | expectedConstants[submatch[2]] = submatch[1] 28 | } 29 | 30 | fset := token.NewFileSet() 31 | if f, err = parser.ParseFile(fset, "", src, 0); err != nil { 32 | t.Error(err) 33 | } 34 | 35 | // Type-check the package. 36 | // We create an empty map for each kind of input 37 | // we're interested in, and Check populates them. 38 | info := types.Info{Defs: make(map[*ast.Ident]types.Object)} 39 | conf := types.Config{Importer: importer.Default()} 40 | if _, err = conf.Check("", fset, []*ast.File{f}, &info); err != nil { 41 | t.Error(err) 42 | } 43 | 44 | actualConstants := make(map[string]string) 45 | for id, obj := range info.Defs { 46 | if c, ok := obj.(*types.Const); ok { 47 | actualConstants[id.Name] = c.Val().String() 48 | } 49 | } 50 | 51 | // Assertions 52 | 53 | for name, expectedValue := range expectedConstants { 54 | if value, ok := actualConstants[name]; !ok { 55 | t.Errorf("exit.go does not define the exit code %q (%s)", name, expectedValue) 56 | } else if value != expectedValue { 57 | t.Errorf("exit.go maps %q to %s, README.md expects it to be %s", name, value, expectedValue) 58 | } 59 | } 60 | for name := range actualConstants { 61 | if _, ok := expectedConstants[name]; !ok { 62 | t.Errorf("exit.go defines an undocumented exit code, %q", name) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /docs/sysexits.md: -------------------------------------------------------------------------------- 1 | # Comparison to sysexits.h Exit Codes 2 | 3 | | Semantic Exit Code | sysexits.h Exit Code | Name | Meaning | 4 | | --: | --: | :-- | :-- | 5 | | 80 | 64 | **Usage Error** | A required argument was omitted or an invalid value was supplied for a flag. | 6 | | 81 | | **Unknown Command** | An unrecognized subcommand was invoked. | 7 | | 82 | | **Requirement Not Met** | A prerequisite wasn't met. | 8 | | 83 | 77 | **Forbidden** | The user isn't authorized to perform the requested action. | 9 | | 84 | | **Moved Permanently** | The program has been migrated to a new location. | 10 | | | 65 | **Data Error** | The input data was incorrect in some way. This should only be used for user's data and not system file. | 11 | | | 66 | **No Input** | An input file (not a system file) did not exist or was not readable. This could also include errors like "No message" to a mailer (if it cared to catch it). | 12 | | | 67 | **No User** | The user specified did not exist. This might be used for mail addresses or remote logins. | 13 | | | 68 | **No Host** | The host specified did not exist. This is used in mail addresses of network requests. | 14 | | | 73 | **Cannot Create** | A (user specified) output file cannot be created. | 15 | | | 78 | **Config Error** | Something was found in an unconfigured or misconfigured state. | 16 | | 100 | 70 | **Internal Error** | An error occurred in the program's own code or in one of its dependencies. | 17 | | 101 | 69 | **Unavailable** | A local daemon or remote service did not respond, a connection was closed unexpectedly, an HTTP service responded with 503. | 18 | | | 71 | **Os Error** | An operating system error has been detected. This is intended to be used for such things as "cannot fork", "cannot create pipe", or the like. It includes things like getuid returning a user that does not exist in the passwd file. | 19 | | | 72 | **Os File Error** | Some system file (e.g., `/etc/passwd`, `/var/run/utmp`, etc.) does not exist, cannot be opened, or has some sort of error (e.g., syntax error). | 20 | | | 74 | **Io Error** | An error occurred while doing I/O on some file. | 21 | | | 75 | **Temporary Failure** | Temporary failure, indicating something that is not really an error. In `sendmail`, this means that a mailer (e.g.) could not create a connection, and the request should be reattempted later. | 22 | | | 76 | **Protocol Error** | The remote system returned something that was "not possible" during a protocol exchanged. | 23 | 24 |
25 | 26 | #### Why did we depart from the `sysexits.h` codes? 27 | 28 | This library's goals are to define exit codes that are: 29 | 1. Broadly applicable to heterogenous Command Line tools 30 | 2. Easy to partition into user errors and system errors 31 | 32 | `sendmail`'s exit codes weren't ideally suited to either goal. While several of its codes are broadly applicable (like **Usage Error**, **Internal Error**, and **Forbidden**), others are [over-fit](https://en.wikipedia.org/wiki/Overfitting) to `sendmail` (like **No User** and **No Host**). And its numbering scheme interleaves user errors with software errors. 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Semantic Exit Codes 2 | 3 | ### Usage 4 | 5 | #### Go 6 | 7 | ```golang 8 | os.Exit(exit.Forbidden) // The user isn't permitted to perform this action 9 | os.Exit(exit.Unavailable) // An API this program consumes isn't available 10 | ``` 11 | 12 | #### Rust 13 | 14 | ```rust 15 | use semantic_exit::{exit, Code}; 16 | 17 | exit(Code::Forbidden); 18 | exit(Code::Unavailable); 19 | ``` 20 | 21 | #### Python 22 | 23 | ```python 24 | import exit 25 | 26 | exit.Code.Forbidden.exit() 27 | exit.Code.Unavailable.exit() 28 | ``` 29 | 30 | See [the complete list of exit codes](#the-codes). 31 | 32 | ### About 33 | 34 | Conventionally, exiting a program with zero indicates success while nonzero indicates failure. 35 | 36 | ```golang 37 | os.Exit(0) // success 38 | os.Exit(1) // failure 39 | ``` 40 | 41 | But the system call `exit` accepts values between 0 and 255, leaving 254 different ways of expressing failure. 42 | 43 | This library's goals are to define exit codes that are: 44 | 1. Broadly applicable to heterogenous Command Line tools 45 | 2. Easy to partition into user errors and system errors 46 | 47 | It defines codes in two [unreserved](#reserved-codes-and-prior-art) ranges: 80-99 for user errors and 100-119 for software or system errors. 48 | 49 | ### The Codes 50 | 51 | | Exit Code | Name | Meaning | 52 | | --: | :-- | :-- | 53 | | 0 | `OK` | The program exited successfully. | 54 | | 1 | `NotOK` | The program exited unsuccessfully but gives no extra context as to what the failure was. | 55 | | 80 | `UsageError` | The program exited unsuccessfully because it was was used incorrectly. (e.g. a required argument was omitted or an invalid value was supplied for a flag.) | 56 | | 81 | `UnknownSubcommand` | The program exited unsuccessfully because an unrecognized subcommand was invoked. (Used by CLI multi-tools.) | 57 | | 82 | `RequirementNotMet` | The program exited unsuccessfully because a prerequisite of it wasn't met. | 58 | | 83 | `Forbidden` | The program exited unsuccessfully because the user isn't authorized to perform the requested action. | 59 | | 84 | `MovedPermanently` | The program exited unsuccessfully because it has been migrated to a new location. | 60 | | 100 | `InternalError` | The program exited unsuccessfully because of a problem in its own code. (Used instead of 1 when the problem is known to be with the program's code or dependencies.) | 61 | | 101 | `Unavailable` | The program exited unsuccessfully because a service it depends on was not available. (e.g. A local daemon or remote service did not respond, a connection was closed unexpectedly, an HTTP service responded with 503.) | 62 | 63 | ### Reserved Codes and Prior Art 64 | 65 | - Values above 128 are reserved for signals. (When a program is terminated with a signal, its exit code is 128 + the signal's numeric value. When you terminate a program with `Ctrl` `C`, for example, you send it the signal `SIGINT` — whose value is 2 — and the program exits with 130.) 66 | - Bash [reserves 2, 126, and 127](https://tldp.org/LDP/abs/html/exitcodes.html). 67 | - [sysexits.h defines 64-78](https://github.com/bminor/glibc/blob/master/misc/sysexits.h#L96-L110). The `sysexits.h` codes were originally defined for `sendmail` but have been used many places since. ([Compare Semantic Exit Codes to sysexits.h codes](./docs/sysexits.md)) 68 | -------------------------------------------------------------------------------- /examples-rs/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "autocfg" 7 | version = "1.1.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 10 | 11 | [[package]] 12 | name = "examples-rs" 13 | version = "0.1.0" 14 | dependencies = [ 15 | "semantic-exit", 16 | "signal-hook", 17 | ] 18 | 19 | [[package]] 20 | name = "libc" 21 | version = "0.2.139" 22 | source = "registry+https://github.com/rust-lang/crates.io-index" 23 | checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" 24 | 25 | [[package]] 26 | name = "num-derive" 27 | version = "0.3.3" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" 30 | dependencies = [ 31 | "proc-macro2", 32 | "quote", 33 | "syn", 34 | ] 35 | 36 | [[package]] 37 | name = "num-traits" 38 | version = "0.2.15" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 41 | dependencies = [ 42 | "autocfg", 43 | ] 44 | 45 | [[package]] 46 | name = "proc-macro2" 47 | version = "1.0.49" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" 50 | dependencies = [ 51 | "unicode-ident", 52 | ] 53 | 54 | [[package]] 55 | name = "quote" 56 | version = "1.0.23" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" 59 | dependencies = [ 60 | "proc-macro2", 61 | ] 62 | 63 | [[package]] 64 | name = "semantic-exit" 65 | version = "1.0.0" 66 | dependencies = [ 67 | "num-derive", 68 | "num-traits", 69 | "thiserror", 70 | ] 71 | 72 | [[package]] 73 | name = "signal-hook" 74 | version = "0.3.14" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" 77 | dependencies = [ 78 | "libc", 79 | "signal-hook-registry", 80 | ] 81 | 82 | [[package]] 83 | name = "signal-hook-registry" 84 | version = "1.4.0" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" 87 | dependencies = [ 88 | "libc", 89 | ] 90 | 91 | [[package]] 92 | name = "syn" 93 | version = "1.0.107" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" 96 | dependencies = [ 97 | "proc-macro2", 98 | "quote", 99 | "unicode-ident", 100 | ] 101 | 102 | [[package]] 103 | name = "thiserror" 104 | version = "1.0.38" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" 107 | dependencies = [ 108 | "thiserror-impl", 109 | ] 110 | 111 | [[package]] 112 | name = "thiserror-impl" 113 | version = "1.0.38" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" 116 | dependencies = [ 117 | "proc-macro2", 118 | "quote", 119 | "syn", 120 | ] 121 | 122 | [[package]] 123 | name = "unicode-ident" 124 | version = "1.0.6" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" 127 | -------------------------------------------------------------------------------- /exit.py: -------------------------------------------------------------------------------- 1 | """ 2 | Defines semantic exit codes which may be used by Command Line tools to aid in 3 | debugging and instrumentation. 4 | 5 | This package defines exit codes in two ranges. Exit Codes 80-99 indicate a user error 6 | of some sort. Exit Codes 100-119 indicate software or system error of some sort. 7 | """ 8 | 9 | import sys 10 | from enum import Enum 11 | 12 | 13 | # Code is the exit code that is passed to the system call `exit` when the 14 | # program terminates. Conventionally, the value zero indicates success and all 15 | # other values (1-255) indicate failure. 16 | class Code(Enum): 17 | # there doesn't really appear to be any convention about letter casing 18 | # here, so I'm adopting the Go format 19 | 20 | # Indicates that the program exited successfully. 21 | OK = 0 22 | 23 | # Indicates that the program exited unsuccessfully but gives no extra 24 | # context as to what the failure was. 25 | NotOK = 1 26 | 27 | # Exit Codes 80-99 are reserved for user errors. 28 | 29 | # Indicates that the program exited unsuccessfully because it was used 30 | # incorrectly. 31 | # 32 | # Examples: a required argument was omitted or an invalid value was 33 | # supplied for a flag. 34 | UsageError = 80 35 | 36 | # Indicates that the program exited unsuccessfully because an unrecognized 37 | # subcommand was invoked. 38 | # 39 | # This is intended for CLI multi-tools. When you run a command that doesn't 40 | # exist from the shell, the shell exits 127. This is distinct from that 41 | # value in that the command itself exists but the subcommand does not (e.g. 42 | # `git nope` could exit 81). 43 | UnknownSubcommand = 81 44 | 45 | # Indicates that the program exited unsuccessfully because a precondition 46 | # wasn't satisfied. 47 | # 48 | # Examples: the user must be on a VPN before using the program or have 49 | # a minimum version of some other software installed. 50 | RequirementNotMet = 82 51 | 52 | # Indicates that the program exited unsuccessfully because the user isn't 53 | # authorized to perform the requested action. 54 | Forbidden = 83 55 | 56 | # Indicates that the program exited unsuccessfully because it has been 57 | # migrated to a new location. 58 | MovedPermanently = 84 59 | 60 | # Exit Codes 100-119 are reserved for software or system errors. 61 | 62 | # Indicates that the program exited unsuccessfully because of a problem in 63 | # its own code. 64 | # 65 | # Used instead of 1 when the problem is known to be with the program's 66 | # code or dependencies. 67 | InternalError = 100 68 | 69 | # Indicates that the program exited unsuccessfully because a service it 70 | # depends on was not available. 71 | # 72 | # Examples: A local daemon or remote service did not respond, a connection 73 | # was closed unexpectedly, an HTTP service responded with 503. 74 | Unavailable = 101 75 | 76 | def is_ok(self): 77 | """Reports whether an exit code is okay. 78 | 79 | Returns True if the code is 0. 80 | """ 81 | 82 | return self.value == 0 83 | 84 | def is_error(self): 85 | """Reports whether an exit code is an error. 86 | 87 | Returns True if the code is *not* 0. 88 | """ 89 | 90 | return self.value != 0 91 | 92 | def is_user_error(self): 93 | """Reports whether an exit code is a user error. 94 | 95 | Returns True if the code is in the range 80-99 and False otherwise. 96 | """ 97 | 98 | return 80 <= self.value <= 99 99 | 100 | def is_software_error(self): 101 | """Reports whether an exit code is a software error. 102 | 103 | Returns True if the code is in the range 100-119 and False otherwise. 104 | """ 105 | 106 | return 100 <= self.value <= 119 107 | 108 | def is_signal(self): 109 | """Reports whether an exit code is derived from a signal. 110 | 111 | Returns True if the code is in the range 128-255 and False if not. 112 | """ 113 | 114 | return 128 < self.value < 255 115 | 116 | def exit(self): 117 | """Invoke sys.exit() with this as an exit code.""" 118 | 119 | sys.exit(self.value) 120 | -------------------------------------------------------------------------------- /exit.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package exit defines semantic exit codes which may be used by Command Line tools 3 | to aid in debugging and instrumentation. 4 | 5 | This package defines exit codes in two ranges. Exit Codes 80-99 indicate a user error 6 | of some sort. Exit Codes 100-119 indicate software or system error of some sort. 7 | */ 8 | package exit 9 | 10 | import ( 11 | "errors" 12 | "fmt" 13 | ) 14 | 15 | // Code is the exit code that is passed to the system call `exit` 16 | // when the program terminates. Conventionally, the value zero indicates 17 | // success and all other values (1-255) indicate failure. 18 | type Code = int 19 | 20 | const ( 21 | // OK indicates that the program exited successfully. 22 | OK Code = 0 23 | 24 | // NotOK indicates that the program exited unsuccessfully 25 | // but gives no extra context as to what the failure was. 26 | NotOK Code = 1 27 | ) 28 | 29 | // Exit Codes 80-99 are reserved for user errors. 30 | const ( 31 | // UsageError indicates that the program exited unsuccessfully 32 | // because it was used incorrectly. 33 | // 34 | // Examples: a required argument was omitted or an invalid value 35 | // was supplied for a flag. 36 | UsageError Code = 80 37 | 38 | // UnknownSubcommand indicates that the program exited unsuccessfully 39 | // because an unrecognized subcommand was invoked. 40 | // 41 | // This is intended for CLI multi-tools. When you run a command that 42 | // doesn't exist from the shell, the shell exits 127. This is distinct 43 | // from that value in that the command itself exists but the subcommand 44 | // does not (e.g. `git nope` could exit 81). 45 | UnknownSubcommand Code = 81 46 | 47 | // RequirementNotMet indicates that the program exited unsuccessfully 48 | // because a precondition wasn't satisfied. 49 | // 50 | // Examples: the user must be on a VPN before using the program or have 51 | // a minimum version of some other software installed. 52 | RequirementNotMet Code = 82 53 | 54 | // Forbidden indicates that the program exited unsuccessfully 55 | // because the user isn't authorized to perform the requested action. 56 | Forbidden Code = 83 57 | 58 | // MovedPermanently indicates that the program exited unsuccessfully 59 | // because it has been migrated to a new location. 60 | MovedPermanently Code = 84 61 | ) 62 | 63 | // Exit Codes 100-119 are reserved for software or system errors. 64 | const ( 65 | // InternalError indicates that the program exited unsuccessfully 66 | // because of a problem in its own code. 67 | // 68 | // Used instead of 1 when the problem is known to be with the program's 69 | // code or dependencies. 70 | InternalError Code = 100 71 | 72 | // Unavailable indicates that the program exited unsuccessfully 73 | // because a service it depends on was not available. 74 | // 75 | // Examples: A local daemon or remote service did not respond, a connection 76 | // was closed unexpectedly, an HTTP service responded with 503. 77 | Unavailable Code = 101 78 | ) 79 | 80 | // IsUserError reports whether an exit code is a user error. 81 | // It returns true if the code is in the range 80-99 and false if not. 82 | func IsUserError(code Code) bool { 83 | return code >= 80 && code <= 99 84 | } 85 | 86 | // IsSoftwareError reports whether an exit code is a software error. 87 | // It returns true if the code is in the range 100-119 and false if not. 88 | func IsSoftwareError(code Code) bool { 89 | return code >= 100 && code <= 119 90 | } 91 | 92 | // IsSignal reports whether an exit code is derived from a signal. 93 | // It returns true if the code is in the range 128-255 and false if not. 94 | // It also returns true if the code is -1 because ProcessState.ExitCode() 95 | // may return -1 if the process was terminated by a signal. 96 | func IsSignal(code Code) bool { 97 | // -1 is not a valid exit code, but ProcessState.ExitCode() 98 | // may return -1 if the process was terminated by a signal. 99 | // 100 | // https://pkg.go.dev/os#ProcessState.ExitCode 101 | return code == -1 || code > 128 && code < 255 102 | } 103 | 104 | var ( 105 | ErrNotOK = Error{Code: NotOK} 106 | ErrUsageError = Error{Code: UsageError} 107 | ErrUnknownSubcommand = Error{Code: UnknownSubcommand} 108 | ErrRequirementNotMet = Error{Code: RequirementNotMet} 109 | ErrForbidden = Error{Code: Forbidden} 110 | ErrMovedPermanently = Error{Code: MovedPermanently} 111 | ErrInternalError = Error{Code: InternalError} 112 | ErrUnavailable = Error{Code: Unavailable} 113 | ) 114 | 115 | func FromError(err error) Code { 116 | var e interface{ ExitCode() int } 117 | 118 | if errors.As(err, &e) { 119 | return e.ExitCode() 120 | } else if err == nil { 121 | return OK 122 | } else { 123 | return NotOK 124 | } 125 | } 126 | 127 | func WrapIf(err error, code Code) error { 128 | if err == nil { 129 | return nil 130 | } 131 | return Wrap(err, code) 132 | } 133 | 134 | func Wrap(err error, code Code) error { 135 | return Error{Code: code, Cause: err} 136 | } 137 | 138 | type Error struct { 139 | Code Code 140 | Cause error 141 | } 142 | 143 | func (e Error) Error() string { 144 | if e.Cause != nil { 145 | return e.Cause.Error() 146 | } else { 147 | return fmt.Sprintf("exit %d", e.Code) 148 | } 149 | } 150 | 151 | func (e Error) Unwrap() error { 152 | return e.Cause 153 | } 154 | 155 | func (e Error) ExitCode() int { 156 | return int(e.Code) 157 | } 158 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Defines semantic exit codes which may be used by Command Line tools to aid in debugging and instrumentation. 2 | //! 3 | //! This package defines exit codes in two ranges: 4 | //! - Exit Codes 80-99 indicate a user error of some sort. 5 | //! - Exit Codes 100-119 indicate software or system error of some sort. 6 | 7 | /// The exit code that is passed to the system call `exit` when the program terminates. 8 | /// Conventionally, the value zero indicates success and all other values (1-255) indicate failure. 9 | #[derive(Clone, Copy, Debug, PartialEq, num_derive::FromPrimitive, num_derive::ToPrimitive)] 10 | #[repr(i32)] 11 | pub enum Code { 12 | /// Indicates that the program exited successfully. 13 | OK = 0, 14 | 15 | /// Indicates that the program exited unsuccessfully but gives no extra context as to what the failure was. 16 | NotOK = 1, 17 | 18 | // Exit Codes 80-99 are reserved for user errors. 19 | /// UsageError indicates that the program exited unsuccessfully because it was was used incorrectly. 20 | /// 21 | /// Examples: a required argument was omitted or an invalid value was supplied for a flag. 22 | UsageError = 80, 23 | 24 | /// Indicates that the program exited unsuccessfully because an unrecognized subcommand was invoked. 25 | /// 26 | /// This is intended for CLI multi-tools. 27 | /// When you run a command that doesn't exist from the shell, the shell exits 127. 28 | /// This is distinct from that value in that the command itself exists but the subcommand does not (e.g. `git nope` could exit 81). 29 | UnknownSubcommand = 81, 30 | 31 | /// Indicates that the program exited unsuccessfully because a precondition wasn't satisfied. 32 | /// 33 | /// Examples: the user must be on a VPN before using the program or have a minimum version of some other software installed. 34 | RequirementNotMet = 82, 35 | 36 | /// Indicates that the program exited unsuccessfully because the user isn't authorized to perform the requested action. 37 | Forbidden = 83, 38 | 39 | /// Indicates that the program exited unsuccessfully because it has been migrated to a new location. 40 | MovedPermanently = 84, 41 | 42 | // Exit Codes 100-119 are reserved for software or system errors. 43 | /// Indicates that the program exited unsuccessfully because of a problem in its own code. 44 | /// 45 | /// Used instead of 1 when the problem is known to be with the program's code or dependencies. 46 | InternalError = 100, 47 | 48 | /// Indicates that the program exited unsuccessfully because a service it depends on was not available. 49 | /// 50 | /// Examples: A local daemon or remote service did not respond, a connection was closed unexpectedly, an HTTP service responded with 503. 51 | Unavailable = 101, 52 | } 53 | 54 | #[derive(Debug, thiserror::Error, PartialEq)] 55 | pub enum Error { 56 | #[error("unknown exit code: {0}")] 57 | UnknownExitCode(i32), 58 | } 59 | 60 | /// Reports whether an exit code is a user error. 61 | /// It returns true if the code is in the range 80-99 and false if not. 62 | pub fn is_user_error(code: Code) -> bool { 63 | (code as i32) >= 80 && (code as i32) <= 99 64 | } 65 | 66 | /// Reports whether an exit code is a software error. 67 | /// It returns true if the code is in the range 100-119 and false if not. 68 | pub fn is_software_error(code: Code) -> bool { 69 | (code as i32) >= 100 && (code as i32) <= 119 70 | } 71 | 72 | /// Reports whether an exit code is derived from a signal. 73 | /// It returns true if the code is in the range 128-255 and false if not. 74 | pub fn is_signal(code: Code) -> bool { 75 | (code as i32) > 128 && (code as i32) < 255 76 | } 77 | 78 | /// Returns the exit code that corresponds to when a program exits in response to a signal. 79 | pub fn from_signal(signal: i32) -> i32 { 80 | 128 + signal 81 | } 82 | 83 | impl TryFrom for Code { 84 | type Error = Error; 85 | 86 | fn try_from(value: i32) -> Result { 87 | num_traits::FromPrimitive::from_i32(value).ok_or(Error::UnknownExitCode(value)) 88 | } 89 | } 90 | 91 | impl From for i32 { 92 | fn from(value: Code) -> Self { 93 | value as i32 94 | } 95 | } 96 | 97 | pub fn exit(code: Code) { 98 | std::process::exit(code.into()); 99 | } 100 | 101 | #[cfg(test)] 102 | mod tests { 103 | use super::*; 104 | 105 | #[test] 106 | fn test_exit_code_match_readme() { 107 | let readme = include_str!("../README.md"); 108 | let re = regex::Regex::new(r"\| (\d+) \| `(\w+)` \| .* \|\n").unwrap(); 109 | for captures in re.captures_iter(readme) { 110 | let expected_code_num: i32 = captures.get(1).unwrap().as_str().parse().unwrap(); 111 | let expected_name = captures.get(2).unwrap().as_str(); 112 | let actual_code: Code = expected_code_num.try_into().expect(&format!( 113 | "does not define the exit code {expected_name} ({expected_code_num})" 114 | )); 115 | let actual_name = format!("{:?}", actual_code); 116 | 117 | assert_eq!(expected_name, actual_name, "maps {actual_name} to {expected_code_num}, README.md expected it to be {expected_name}"); 118 | } 119 | } 120 | 121 | #[test] 122 | fn test_unknown_exit_code() { 123 | let err = >::try_into(-1).unwrap_err(); 124 | assert_eq!(err, Error::UnknownExitCode(-1)) 125 | } 126 | 127 | #[test_case::test_case(libc::SIGINT, 130)] 128 | fn test_from_signal(signal: i32, expected: i32) { 129 | assert_eq!(from_signal(signal), expected); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.0.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "autocfg" 16 | version = "1.1.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 19 | 20 | [[package]] 21 | name = "cfg-if" 22 | version = "1.0.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 25 | 26 | [[package]] 27 | name = "libc" 28 | version = "0.2.177" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" 31 | 32 | [[package]] 33 | name = "memchr" 34 | version = "2.6.3" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" 37 | 38 | [[package]] 39 | name = "num-derive" 40 | version = "0.4.2" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" 43 | dependencies = [ 44 | "proc-macro2", 45 | "quote", 46 | "syn 2.0.87", 47 | ] 48 | 49 | [[package]] 50 | name = "num-traits" 51 | version = "0.2.19" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 54 | dependencies = [ 55 | "autocfg", 56 | ] 57 | 58 | [[package]] 59 | name = "proc-macro-error" 60 | version = "1.0.4" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 63 | dependencies = [ 64 | "proc-macro-error-attr", 65 | "proc-macro2", 66 | "quote", 67 | "syn 1.0.107", 68 | "version_check", 69 | ] 70 | 71 | [[package]] 72 | name = "proc-macro-error-attr" 73 | version = "1.0.4" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 76 | dependencies = [ 77 | "proc-macro2", 78 | "quote", 79 | "version_check", 80 | ] 81 | 82 | [[package]] 83 | name = "proc-macro2" 84 | version = "1.0.89" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" 87 | dependencies = [ 88 | "unicode-ident", 89 | ] 90 | 91 | [[package]] 92 | name = "quote" 93 | version = "1.0.35" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 96 | dependencies = [ 97 | "proc-macro2", 98 | ] 99 | 100 | [[package]] 101 | name = "regex" 102 | version = "1.12.2" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" 105 | dependencies = [ 106 | "aho-corasick", 107 | "memchr", 108 | "regex-automata", 109 | "regex-syntax", 110 | ] 111 | 112 | [[package]] 113 | name = "regex-automata" 114 | version = "0.4.12" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "722166aa0d7438abbaa4d5cc2c649dac844e8c56d82fb3d33e9c34b5cd268fc6" 117 | dependencies = [ 118 | "aho-corasick", 119 | "memchr", 120 | "regex-syntax", 121 | ] 122 | 123 | [[package]] 124 | name = "regex-syntax" 125 | version = "0.8.5" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 128 | 129 | [[package]] 130 | name = "semantic-exit" 131 | version = "1.0.0" 132 | dependencies = [ 133 | "libc", 134 | "num-derive", 135 | "num-traits", 136 | "regex", 137 | "test-case", 138 | "thiserror", 139 | ] 140 | 141 | [[package]] 142 | name = "syn" 143 | version = "1.0.107" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" 146 | dependencies = [ 147 | "proc-macro2", 148 | "unicode-ident", 149 | ] 150 | 151 | [[package]] 152 | name = "syn" 153 | version = "2.0.87" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" 156 | dependencies = [ 157 | "proc-macro2", 158 | "quote", 159 | "unicode-ident", 160 | ] 161 | 162 | [[package]] 163 | name = "test-case" 164 | version = "3.3.1" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8" 167 | dependencies = [ 168 | "test-case-macros", 169 | ] 170 | 171 | [[package]] 172 | name = "test-case-core" 173 | version = "3.2.1" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "54c25e2cb8f5fcd7318157634e8838aa6f7e4715c96637f969fabaccd1ef5462" 176 | dependencies = [ 177 | "cfg-if", 178 | "proc-macro-error", 179 | "proc-macro2", 180 | "quote", 181 | "syn 2.0.87", 182 | ] 183 | 184 | [[package]] 185 | name = "test-case-macros" 186 | version = "3.2.1" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "37cfd7bbc88a0104e304229fba519bdc45501a30b760fb72240342f1289ad257" 189 | dependencies = [ 190 | "proc-macro-error", 191 | "proc-macro2", 192 | "quote", 193 | "syn 2.0.87", 194 | "test-case-core", 195 | ] 196 | 197 | [[package]] 198 | name = "thiserror" 199 | version = "2.0.17" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" 202 | dependencies = [ 203 | "thiserror-impl", 204 | ] 205 | 206 | [[package]] 207 | name = "thiserror-impl" 208 | version = "2.0.17" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" 211 | dependencies = [ 212 | "proc-macro2", 213 | "quote", 214 | "syn 2.0.87", 215 | ] 216 | 217 | [[package]] 218 | name = "unicode-ident" 219 | version = "1.0.6" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" 222 | 223 | [[package]] 224 | name = "version_check" 225 | version = "0.9.4" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 228 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "attrs" 5 | version = "22.2.0" 6 | description = "Classes Without Boilerplate" 7 | optional = false 8 | python-versions = ">=3.6" 9 | files = [ 10 | {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, 11 | {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, 12 | ] 13 | 14 | [package.extras] 15 | cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] 16 | dev = ["attrs[docs,tests]"] 17 | docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] 18 | tests = ["attrs[tests-no-zope]", "zope.interface"] 19 | tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] 20 | 21 | [[package]] 22 | name = "black" 23 | version = "24.3.0" 24 | description = "The uncompromising code formatter." 25 | optional = false 26 | python-versions = ">=3.8" 27 | files = [ 28 | {file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"}, 29 | {file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"}, 30 | {file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"}, 31 | {file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"}, 32 | {file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"}, 33 | {file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"}, 34 | {file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"}, 35 | {file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"}, 36 | {file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"}, 37 | {file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"}, 38 | {file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"}, 39 | {file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"}, 40 | {file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"}, 41 | {file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"}, 42 | {file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"}, 43 | {file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"}, 44 | {file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"}, 45 | {file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"}, 46 | {file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"}, 47 | {file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"}, 48 | {file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"}, 49 | {file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"}, 50 | ] 51 | 52 | [package.dependencies] 53 | click = ">=8.0.0" 54 | mypy-extensions = ">=0.4.3" 55 | packaging = ">=22.0" 56 | pathspec = ">=0.9.0" 57 | platformdirs = ">=2" 58 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 59 | typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} 60 | 61 | [package.extras] 62 | colorama = ["colorama (>=0.4.3)"] 63 | d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] 64 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 65 | uvloop = ["uvloop (>=0.15.2)"] 66 | 67 | [[package]] 68 | name = "click" 69 | version = "8.1.3" 70 | description = "Composable command line interface toolkit" 71 | optional = false 72 | python-versions = ">=3.7" 73 | files = [ 74 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 75 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 76 | ] 77 | 78 | [package.dependencies] 79 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 80 | 81 | [[package]] 82 | name = "colorama" 83 | version = "0.4.6" 84 | description = "Cross-platform colored terminal text." 85 | optional = false 86 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 87 | files = [ 88 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 89 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 90 | ] 91 | 92 | [[package]] 93 | name = "exceptiongroup" 94 | version = "1.1.0" 95 | description = "Backport of PEP 654 (exception groups)" 96 | optional = false 97 | python-versions = ">=3.7" 98 | files = [ 99 | {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"}, 100 | {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"}, 101 | ] 102 | 103 | [package.extras] 104 | test = ["pytest (>=6)"] 105 | 106 | [[package]] 107 | name = "flake8" 108 | version = "6.0.0" 109 | description = "the modular source code checker: pep8 pyflakes and co" 110 | optional = false 111 | python-versions = ">=3.8.1" 112 | files = [ 113 | {file = "flake8-6.0.0-py2.py3-none-any.whl", hash = "sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7"}, 114 | {file = "flake8-6.0.0.tar.gz", hash = "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181"}, 115 | ] 116 | 117 | [package.dependencies] 118 | mccabe = ">=0.7.0,<0.8.0" 119 | pycodestyle = ">=2.10.0,<2.11.0" 120 | pyflakes = ">=3.0.0,<3.1.0" 121 | 122 | [[package]] 123 | name = "iniconfig" 124 | version = "2.0.0" 125 | description = "brain-dead simple config-ini parsing" 126 | optional = false 127 | python-versions = ">=3.7" 128 | files = [ 129 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 130 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 131 | ] 132 | 133 | [[package]] 134 | name = "isort" 135 | version = "5.11.4" 136 | description = "A Python utility / library to sort Python imports." 137 | optional = false 138 | python-versions = ">=3.7.0" 139 | files = [ 140 | {file = "isort-5.11.4-py3-none-any.whl", hash = "sha256:c033fd0edb91000a7f09527fe5c75321878f98322a77ddcc81adbd83724afb7b"}, 141 | {file = "isort-5.11.4.tar.gz", hash = "sha256:6db30c5ded9815d813932c04c2f85a360bcdd35fed496f4d8f35495ef0a261b6"}, 142 | ] 143 | 144 | [package.extras] 145 | colors = ["colorama (>=0.4.3,<0.5.0)"] 146 | pipfile-deprecated-finder = ["pipreqs", "requirementslib"] 147 | plugins = ["setuptools"] 148 | requirements-deprecated-finder = ["pip-api", "pipreqs"] 149 | 150 | [[package]] 151 | name = "mccabe" 152 | version = "0.7.0" 153 | description = "McCabe checker, plugin for flake8" 154 | optional = false 155 | python-versions = ">=3.6" 156 | files = [ 157 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 158 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 159 | ] 160 | 161 | [[package]] 162 | name = "mypy-extensions" 163 | version = "0.4.3" 164 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 165 | optional = false 166 | python-versions = "*" 167 | files = [ 168 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 169 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 170 | ] 171 | 172 | [[package]] 173 | name = "packaging" 174 | version = "23.0" 175 | description = "Core utilities for Python packages" 176 | optional = false 177 | python-versions = ">=3.7" 178 | files = [ 179 | {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, 180 | {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, 181 | ] 182 | 183 | [[package]] 184 | name = "pathspec" 185 | version = "0.10.3" 186 | description = "Utility library for gitignore style pattern matching of file paths." 187 | optional = false 188 | python-versions = ">=3.7" 189 | files = [ 190 | {file = "pathspec-0.10.3-py3-none-any.whl", hash = "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6"}, 191 | {file = "pathspec-0.10.3.tar.gz", hash = "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6"}, 192 | ] 193 | 194 | [[package]] 195 | name = "platformdirs" 196 | version = "2.6.2" 197 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 198 | optional = false 199 | python-versions = ">=3.7" 200 | files = [ 201 | {file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"}, 202 | {file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"}, 203 | ] 204 | 205 | [package.extras] 206 | docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] 207 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] 208 | 209 | [[package]] 210 | name = "pluggy" 211 | version = "1.0.0" 212 | description = "plugin and hook calling mechanisms for python" 213 | optional = false 214 | python-versions = ">=3.6" 215 | files = [ 216 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 217 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 218 | ] 219 | 220 | [package.extras] 221 | dev = ["pre-commit", "tox"] 222 | testing = ["pytest", "pytest-benchmark"] 223 | 224 | [[package]] 225 | name = "pycodestyle" 226 | version = "2.10.0" 227 | description = "Python style guide checker" 228 | optional = false 229 | python-versions = ">=3.6" 230 | files = [ 231 | {file = "pycodestyle-2.10.0-py2.py3-none-any.whl", hash = "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610"}, 232 | {file = "pycodestyle-2.10.0.tar.gz", hash = "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053"}, 233 | ] 234 | 235 | [[package]] 236 | name = "pyflakes" 237 | version = "3.0.1" 238 | description = "passive checker of Python programs" 239 | optional = false 240 | python-versions = ">=3.6" 241 | files = [ 242 | {file = "pyflakes-3.0.1-py2.py3-none-any.whl", hash = "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf"}, 243 | {file = "pyflakes-3.0.1.tar.gz", hash = "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd"}, 244 | ] 245 | 246 | [[package]] 247 | name = "pytest" 248 | version = "7.2.0" 249 | description = "pytest: simple powerful testing with Python" 250 | optional = false 251 | python-versions = ">=3.7" 252 | files = [ 253 | {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"}, 254 | {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"}, 255 | ] 256 | 257 | [package.dependencies] 258 | attrs = ">=19.2.0" 259 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 260 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 261 | iniconfig = "*" 262 | packaging = "*" 263 | pluggy = ">=0.12,<2.0" 264 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 265 | 266 | [package.extras] 267 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] 268 | 269 | [[package]] 270 | name = "tomli" 271 | version = "2.0.1" 272 | description = "A lil' TOML parser" 273 | optional = false 274 | python-versions = ">=3.7" 275 | files = [ 276 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 277 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 278 | ] 279 | 280 | [[package]] 281 | name = "typing-extensions" 282 | version = "4.4.0" 283 | description = "Backported and Experimental Type Hints for Python 3.7+" 284 | optional = false 285 | python-versions = ">=3.7" 286 | files = [ 287 | {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, 288 | {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, 289 | ] 290 | 291 | [metadata] 292 | lock-version = "2.0" 293 | python-versions = "^3.8.1" 294 | content-hash = "3c059ba89b52d3f6b438cb6af5f27baed93dcd5475f403df97f3b1ac9f741264" 295 | --------------------------------------------------------------------------------