Skip to main content

Easily manage data storage and logging across repos

Project description

Cabinet

Cabinet is a lightweight, flexible data organization tool that lets you manage your data with the simplicity of a JSON file or the power of MongoDB - your choice.

Breaking changes in 3.0.0

This release changes how logging works. Plan upgrades accordingly.

  • **logdb() removed.** Use cab.log(...) instead. There is no separate database-only logging API.

  • **--logdb / -ldb removed.** Use cabinet -l / --log with --level as needed; the CLI follows the same rules as the Python API.

  • File-first logging: cab.log() / cabinet --log always write to local-only daily files under path_dir_log on each machine. Logging does not use MongoDB. Optional **logging.loki_enabled** appends one structured JSON line per event to a sibling ***.jsonl** file so Promtail on that same machine can ship to a central Loki (see Logging and Loki (optional)).

  • **cab.log_query() and cabinet --query:** Search log files under path_dir_log only. MongoDB is for Cabinet’s configuration/data collection, not logs. Optional **since** filters by UTC cutoff or timedelta (compared using each line’s local timestamp).

  • **cab.log_query_issues():** Returns WARNING, ERROR, and CRITICAL lines for the last 24 hours by default by scanning today’s and yesterday’s daily files under path_dir_log.

  • **cab.log_query_documents() removed:** Use Loki/Grafana for centralized structured log views, or read **.jsonl** / **.log** files directly.

  • cab.remove() / cabinet --remove: Only applied when MongoDB is enabled. With local JSON storage (mongodb_enabled: false), Cabinet does not change ~/.cabinet/data.json; it prints that removal is MongoDB-only—use cabinet -e / --edit-file or edit the file directly.

  • cab.export() / cabinet --export: Writes a timestamped JSON file under ~/.cabinet/export. With MongoDB, that snapshot matches the cached collection export; with local storage, it writes a pretty-printed copy of ~/.cabinet/data.json.

  • Breaking change in 2.0.0

  • Cabinet

✨ Features

  • Access your data across multiple projects
  • Log messages to local files under path_dir_log (and optional local *.jsonl for that host’s Promtail). **cab.log_query()** / **cab.log_query_issues()** read files; **cab.log_query*_loki()** reads Loki when **logging.loki_url** is set. MongoDB is only for Cabinet data, not logs.
  • Store Cabinet data in MongoDB or ~/.cabinet/data.json; edit it like a JSON document (-e / cab.edit_cabinet()), export snapshots (--export), and use --remove only with MongoDB enabled
  • Send mail from the terminal (SMTP email.port accepts a number or numeric string; email.to may be a string, comma-separated addresses, or a JSON array—same idea as --to on the CLI)
  • Library for interactive command-line interface components using prompt_toolkit

Breaking change in 2.0.0

  • mongodb_connection_string replaces mongodb_username and mongodb_password.

Installation and Setup

CLI and Python Library (Recommended)

  • Install pipx if you don't have it already
  • Install cabinet:
pipx install cabinet
cabinet --configure

Requires Python 3.10+ (see python_requires in setup.cfg).

CLI Only

curl -s https://api.github.com/repos/tylerjwoodfin/cabinet/releases/latest \
| grep "browser_download_url" \
| cut -d '"' -f 4 \
| xargs curl -L -o cabinet.pex

sudo mv cabinet.pex /usr/local/bin/cabinet

Dependencies

Outside of the standard Python library, the following packages are declared as dependencies (setup.cfg / package metadata):

  • pymongo: MongoDB client and related errors.
  • prompt_toolkit: Interactive CLI helpers (cabinet.ui).

For development, install optional test dependencies and run the suite:

pip install -e ".[dev]"
pytest

Structure

  • Data is stored in ~/.cabinet/data.json or MongoDB
    • Data from MongoDB is interacted with as if it were a JSON file.
    • With MongoDB enabled, Cabinet keeps ~/.config/cabinet/cache.json: it is refreshed when missing, when you use --force-cache-update / force_cache_update=True, or when the cache file is older than one hour; otherwise reads use the cache by default.
  • Logs: **cab.log()** / **cabinet --log** append to **path_dir_log/<YYYY-MM-DD>/** on that machine only. Optional JSONL for **logging.loki_enabled** (scraped by Promtail on the same host). **cab.log_query()** / **cab.log_query_issues()** scan local **.log** files. **cab.log_query_loki()** / **cab.log_query_documents_loki()** / **cab.log_query_issues_loki()** query Loki via **logging.loki_url**. MongoDB stores Cabinet variables only, not logs.

CLI usage

Run cabinet --help for the full list (wording matches your installed version). Summary:

Flag Role
--configure / -config Setup; writes ~/.config/cabinet/config.json
--edit / -e Edit Cabinet data as JSON (MongoDB via cache file, or ~/.cabinet/data.json locally)
--edit-file / -ef Edit a path or shortcut under path.edit.*
--force-cache-update Refresh MongoDB cache before reads
--no-create With -ef, do not create a missing file
--get / -g Read nested keys from Cabinet data
--put / -p Write nested keys to Cabinet data
--append / -a Append to a string or list value
--remove / -rm Remove nested keys (MongoDB only; with local JSON, Cabinet prints guidance and does not modify the file)
--get-file Read a file under ~/.cabinet (or configured notes path)
--export Write timestamped JSON to ~/.cabinet/export/ (Mongo snapshot or local data.json)
--strip With --get-file, do not strip whitespace (default strips)
--log / -l, --level, --tags Append to local log files (optional JSONL when logging.loki_enabled)
--editor Override editor for --edit / --edit-file
--query / -q Search log files under path_dir_log (optional log name; default today’s file)
--query-tags, --query-path, --query-hostname, --query-level, --query-date, --query-message Filters for --query
--mail, --subject / -s, --body / -b, --to / -t Send mail; --to may be comma-separated addresses
--version / -v Print package version

--query matches Python Cabinet.log_query() (file-based logs only, not the Cabinet data store).

Configuration

  • Configuration data is stored in ~/.config/cabinet/config.json.
  • Upon first launch, the tool will walk you through each option.
    • path_dir_log is the local directory where logs are stored on this machine by default. If this is a multi-machine setup, do not put this under Syncthing, NFS, or other shared folders (see Logging and Loki). A practical default is ~/.local/share/cabinet/log, which stays off most Syncthing profiles even if you sync ~/.cabinet.
    • mongodb_enabled is a boolean that determines whether MongoDB is used.
    • mongodb_db_name is the name of the database you want to use by default.
    • mongodb_connection_string is the connection string for MongoDB.
    • editor is the default editor that will be used when editing files.
    • You will be prompted to enter your MongoDB credentials (optional).
    • If you choose not to use MongoDB, data will be stored in ~/.cabinet/data.json.
  • Follow these instructions to find your MongoDB connection string: MongoDB Atlas or MongoDB (for local MongoDB, untested).
  • You will be asked to configure your default editor from the list of available editors on your system. If this step is skipped, or an error occurs, nano will be used. You can change this with cabinet --configure and modifying the editor attribute.

Your config.json should look something like this:

{
    "path_dir_log": "/home/you/.local/share/cabinet/log",
    "mongodb_db_name": "cabinet (or other name of your choice)",
    "editor": "nvim",
    "mongodb_enabled": true,
    "mongodb_connection_string": "<your connection string>",
    "logging": {
        "loki_enabled": true,
        "log_dir": "/home/you/.local/share/cabinet/log",
        "loki_url": "http://loki.example.com:3100"
    }
}

Use a directory that exists only on this machine. If you previously used something like ~/syncthing/... for logs, move path_dir_log (and logging.log_dir if set) to a non-synced path such as ~/.local/share/cabinet/log or ~/.cache/cabinet/log.

The **logging** object is optional. loki_enabled turns on parallel JSON Lines files (*.jsonl) next to each *.log on that machine’s local disk for local Promtail to scrape. **loki_url** enables **cab.log_query*_loki(...)** helpers against a Loki server (central or tunneled). **log_dir**, when set, overrides the on-disk log directory for cab.log() (if omitted, top-level **path_dir_log** is used).

Logging and Loki (optional)

Architecture (multi-machine):

log() → local *.log + *.jsonl (this host only) → Promtail (this host) → Loki (one central server) → Grafana
  • File-first: Every cab.log() / cabinet -l line goes to the classic **.log** format on local disk.
  • Do not share log directories: Set path_dir_log / logging.log_dir to a local-only directory on each machine. Do not use Syncthing, NFS, or other shared folders for Cabinet logs: multiple writers to the same files cause sync conflicts, torn lines, duplicate or missing ingestion, and undefined ordering. That pattern is unsupported.
    • Syncthing: Even if Cabinet’s config lives under a synced tree, point path_dir_log at something outside your Syncthing folders (for example ~/.local/share/cabinet/log or ~/.cache/cabinet/log). mkdir -p ~/.local/share/cabinet/log on each host before logging.
  • No log push HTTP: cab.log() never POSTs to Loki; Promtail on each machine tails local *.jsonl and pushes to Loki. Reads from Loki (cab.log_query*_loki) use HTTP only when logging.loki_url is set.
  • Optional JSONL: With logging.loki_enabled, each event also appends one JSON object per line to **LOG_DAILY_<date>.jsonl** in the same date folder (fields include timestamp, level, message, tags, source, hostname from socket.gethostname() on that host).
  • Install Loki, Grafana, and Promtail (this repo’s Docker stack): You need Docker and the Docker Compose plugin on any machine that will run containers. Upstream docs: Grafana Loki, Promtail, source github.com/grafana/loki.
    • Central server (Loki + Grafana): One host runs the stack so all Promtail agents can push to it.
      cd /path/to/your/checkout/docker/loki
      cp .env.example .env    # optional: set GRAFANA_PORT
      docker compose up -d
      
      This starts Loki (port 3100 by default) and Grafana (host port from .env; login admin / admin). It does not start Promtail.
    • Each Cabinet host (Promtail only): In the same docker/loki directory, copy promtail.env.examplepromtail.env, set LOKI_URL (reachable from that host) and CABINET_LOG_DIR (same absolute path as path_dir_log / logging.log_dir in that machine’s Cabinet config), then:
      cd /path/to/your/checkout/docker/loki
      docker compose --env-file promtail.env -f docker-compose.promtail.yml up -d
      
      More detail, including host.docker.internal vs LAN IP for LOKI_URL, is in ../docker/loki/README.md (sibling of this cabinet repo when both live under the same parent directory).
  • Multi-machine example: Host cloud runs Cabinet → local JSONL → Promtail → Loki; host rainbow same; host ice same. In Grafana Explore, filter by source host with LogQL labels such as {job="cabinet", hostname="rainbow"} (hostname comes from the JSON line).
  • Turn off Loki shipping: Set loki_enabled to false, or stop Promtail on that host. Cabinet does not require Docker.
  • Explore logs in Grafana: Explore → datasource Loki → e.g. {job="cabinet"} or {job="cabinet", hostname="cloud"}.
  • Query from Python (Loki): Set logging.loki_url to your reachable Loki base URL—often Tailscale, LAN, or VPN (e.g. http://central:3100, http://cloud.tail….ts.net:3100); avoid a public hostname unless you intentionally expose Loki. Use **cab.log_query_loki(...)**, **cab.log_query_documents_loki(...)**, **cab.log_query_issues_loki(...)**. Optional **logging.loki_job** (default cabinet, must match Promtail’s job label) and **logging.loki_query_timeout** (seconds, default 30).
  • Config example (logging + Loki reads):
    "logging": {
        "loki_enabled": true,
        "log_dir": "/home/you/.local/share/cabinet/log",
        "loki_url": "http://loki.example.com:3100",
        "loki_job": "cabinet",
        "loki_query_timeout": 30
    }
    

edit_file() shortcuts

  • see example below to enable something like
    • cabinet -ef shopping from the terminal
      • rather than cabinet -ef "~/path/to/shopping_list.md"
    • or cabinet.Cabinet().edit("shopping")
      • rather than cabinet.Cabinet().edit("~/path/to/whatever.md")

Shortcut mapping example:

{
  "path": {
    "edit": {
      "shopping": {
        "value": "~/path/to/whatever.md"
      },
      "todo": {
        "value": "~/path/to/whatever.md"
      }
    }
  }
}

set from terminal:

cabinet -p edit shopping value "~/path/to/whatever.md"
cabinet -p edit todo value "~/path/to/whatever.md"

mail (configuration)

  • It is NEVER a good idea to store your password openly either locally or in MongoDB; for this reason, I recommend a "throwaway" account that is only used for sending emails, such as a custom domain email.
  • Gmail and most other mainstream email providers won't work with this; for support, search for sending mail from your email provider with smtplib.
  • email.port: May be a JSON number or a string (e.g. "587"). Cabinet normalizes it to an integer in the valid TCP range.
  • email.to: May be a single address string, comma-separated addresses in one string, or a JSON array of strings (or mix)—same rules as the CLI --to / Mail.send(..., to_addr=...).
  • In Cabinet (cabinet -e), add the email object to make your settings file look like this example:

file:

{
    "email": {
        "from": "throwaway@example.com",
        "from_pw": "example",
        "from_name": "Cabinet (or other name of your choice)",
        "to": "destination@protonmail.com",
        "smtp_server": "example.com",
        "imap_server": "example.com",
        "port": 587
    }
}

set from terminal:

cabinet -p email from throwaway@example.com
cabinet -p email from_pw example
...

Examples

put

python:

from cabinet import Cabinet

cab = Cabinet()

cab.put("employee", "Tyler", "salary", 7.25)

or terminal:

cabinet -p employee Tyler salary 7.25

results in this structure in MongoDB:

{
    "employee": {
        "Tyler": {
            "salary": 7.25 # or "7.25" if done from terminal
        }
    }
}

get

python:

from cabinet import Cabinet

cab = Cabinet()

print(cab.get("employee", "Tyler", "salary"))

# or cab.get("employee", "Tyler", "salary", is_print = True)

or terminal:

cabinet -g employee Tyler salary
  • optional: --force-cache-update to force a cache update

results in:

7.25

remove

python:

from cabinet import Cabinet

cab = Cabinet()

cab.remove("employee", "Tyler", "salary")

or terminal:

cabinet -rm employee Tyler salary

results in this structure in MongoDB (local data.json follows the same shape):

{
    "employee": {
        "Tyler": {}
    }
}

When mongodb_enabled is false, cab.remove() / cabinet --remove do not edit ~/.cabinet/data.json; Cabinet prints that removal requires MongoDB. Use cabinet -e, cabinet --edit-file, or edit the JSON file directly.

export

Writes a timestamped JSON file under ~/.cabinet/export/.

  • MongoDB: Exports the same JSON snapshot used for the editor cache (collection dump).
  • Local: Reads ~/.cabinet/data.json and writes pretty-printed JSON.

python:

from cabinet import Cabinet

Cabinet().export()

terminal:

cabinet --export

Appends a value to an existing string (concatenation) or array. For other types (bool, int, float, etc.), prints a graceful error. Prints the updated value.

python:

from cabinet import Cabinet

cab = Cabinet()

# Append to array: ['apple'] -> ['apple', 'banana']
cab.put("fruits", ["apple"])
cab.append("fruits", "banana", is_print=True)

# Append to string: 'apple' -> 'applebanana'
cab.put("fruits", "apple")
cab.append("fruits", "banana", is_print=True)

or terminal:

# Append to array: ['apple'] -> ['apple', 'banana']
cabinet -p fruits '["apple"]'
cabinet -a fruits banana

# Append to string: 'apple' -> 'applebanana'
cabinet -p fruits apple
cabinet -a fruits banana

# Nested paths
cabinet -a person tyler fruits banana
  • Arrays: Adds the value as a new element. ['apple']['apple', 'banana']
  • Strings: Concatenates. 'apple''applebanana'
  • Other types: Not allowed.
  • Missing key: Not allowed.

edit

  • With MongoDB, opens the JSON cache file that mirrors the cabinet collection; on save, changes are written back to MongoDB.
  • With local storage, opens ~/.cabinet/data.json.

terminal:

# opens file in the default editor (`cabinet --configure` -> 'editor'), saves upon exit
cabinet -e

# or

cabinet --edit

# you can add an 'editor':

cabinet -e --editor=code

edit_file

python:

from cabinet import Cabinet

cab = Cabinet()

# After `path.edit.shopping.value` is set (e.g. `cabinet -p edit shopping value "~/path/to/shopping.md"`), this edits that file:

# opens file in the default editor (`cabinet --configure` -> 'editor'), saves upon exit
cab.edit("shopping")

# or you can edit a file directly...
cab.edit("/path/to/shopping.md")

# you can pass an 'editor' to override the default:
cab.edit("/path/to/shopping.md", editor="nvim")

terminal:

# assumes path -> edit -> shopping -> path/to/shopping.md has been set
cabinet -ef shopping

# or

cabinet -ef "/path/to/shopping.md"

Sending mail

from cabinet import Mail

mail = Mail()

mail.send('Test Subject', 'Test Body')

terminal:

cabinet --mail --subject "Test Subject" --body "Test Body"

# or

cabinet --mail -s "Test Subject" -b "Test Body"

# optional recipients (comma-separated); otherwise `email.to` from Cabinet is used

cabinet --mail -s "Hi" -b "Hello" -t "one@example.com, two@example.com"

If --to / -t is omitted, email.to from Cabinet data is used (see mail configuration).

**cab.log()** / **cabinet --log** write only to local files (classic **.log** lines). There are no network calls. If **logging.loki_enabled** is true in config.json, each event also appends a structured line to a matching local .jsonl file for Promtail on that host to ship to central Loki. Failures to write are printed to stderr and do not propagate as exceptions from log().

MongoDB is only for Cabinet’s **get / put** data. Logs are files (and optional Loki).

from cabinet import Cabinet

cab = Cabinet()

# Always: daily file under path_dir_log, e.g. ~/.cabinet/log/<YYYY-MM-DD>/LOG_DAILY_<date>.log
# If logging.loki_enabled: also ~/.cabinet/log/<YYYY-MM-DD>/LOG_DAILY_<date>.jsonl
cab.log("Connection timed out")  # defaults to info if no level is set
cab.log("Debug breakpoint", level="debug")

cab.log("This function hit a breakpoint", level="debug")
cab.log("Looks like the server is on fire", level="critical")
cab.log("This is fine", level="info")

# Log with tags (optional list of strings)
cab.log("Checked weather successfully", tags=["weather"])
cab.log("Starting Borg Backup...", tags=["backup", "start"])
cab.log("Pruning repository", tags=["backup", "prune"])
cab.log("Compacting repository", tags=["backup", "compact"])

# Custom daily log name / folder (file mode)
cab.log("30", log_name="LOG_TEMPERATURE")
cab.log("30", log_name="LOG_TEMPERATURE", log_folder_path="~/weather")

    # file format (without tags)
    # 2025-10-28 17:01:01,858 — INFO -> tools/weather.py:34@{hostname} -> Checking weather

    # file format (with tags)
    # 2025-09-27 02:01:09,012 — INFO [weather] -> tools/weather.py:116@cloud -> Checked weather successfully

terminal:

# Same file routing as Python (optional JSONL if logging.loki_enabled in config)
cabinet -l "Connection timed out"
cabinet --log "Connection timed out"
cabinet --log "Server is on fire" --level "critical"

cabinet --log "Checked weather successfully" --tags "weather"
cabinet --log "Starting Borg Backup..." --tags "backup,start"
cabinet --log "Pruning repository" --level "info" --tags "backup,prune"

log_query

Query log files under **path_dir_log** by tags, path, hostname, level, date, and message. Optional **since** (datetime or timedelta) restricts to entries on or after that UTC cutoff (using each line’s parsed local time).

from datetime import timedelta
from cabinet import Cabinet

cab = Cabinet()

# Optional time window
results = cab.log_query(since=timedelta(days=7), level="ERROR")

results = cab.log_query(tags=["weather"], level="INFO")

# Query today's file (log_file defaults to today's LOG_DAILY_*.log)
results = cab.log_query(tags=["weather"])

# File mode: query a specific file name
results = cab.log_query("LOG_DAILY_2025-09-27.log", tags=["weather"])

# Query by log level (in today's log)
results = cab.log_query(level="ERROR")

# Query by message content (case-insensitive fuzzy search)
results = cab.log_query(message="repository")

# Query by path (case-insensitive fuzzy search on file path after arrow)
results = cab.log_query(path="cabinet")

# Query by hostname
results = cab.log_query(hostname="cloud")

# Query by date (filters by timestamp in the log entry)
results = cab.log_query(date_filter="2025-09-27")

# Combine multiple filters on today's log
results = cab.log_query(
    tags=["backup"],
    level="INFO",
    message="repository"
)

# Combine multiple filters on specific log file
results = cab.log_query(
    "LOG_DAILY_2025-09-27.log",
    tags=["backup"],
    level="INFO",
    message="repository"
)

terminal:

# Query today's log (defaults to today if no log file specified)
cabinet --query --query-tags "weather"

# Short form
cabinet -q --query-tags "backup"

# Query by level
cabinet --query --query-level "ERROR"

# Query by message content
cabinet --query --query-message "repository"

# Query by path (fuzzy search)
cabinet --query --query-path "tools"

# Query by hostname
cabinet --query --query-hostname "cloud"

# Query by date
cabinet --query --query-date "2025-10-28"

# Combine multiple filters
cabinet --query --query-tags "backup" --query-level "INFO"

# Query specific log file
cabinet --query "LOG_DAILY_2025-09-27.log" --query-tags "weather"

# Complex query with multiple filters
cabinet -q "LOG_DAILY_2025-09-27.log" --query-tags "backup,weather" --query-level "INFO" --query-message "repository"

log_query_issues

Returns formatted log lines at WARNING, ERROR, or CRITICAL within since (timedelta or UTC-aware datetime; default: last 24 hours). Scans today’s and yesterday’s daily **.log** files under **path_dir_log** (same line format as **log_query**).

from datetime import timedelta
from cabinet import Cabinet

cab = Cabinet()

for line in cab.log_query_issues():
    print(line)

# Custom window
lines = cab.log_query_issues(since=timedelta(days=2))

log_query_loki, log_query_documents_loki, log_query_issues_loki

Loki-backed queries (HTTP to **logging.loki_url**). Same filter ideas as **log_query** and **log_query_issues**: since / end, level, hostname, tags, path (source file substring), message, date_filter, log_file (matches Promtail’s **filename** label when present), limit. Returns formatted lines or **dict** rows with **timestamp** as **datetime** (UTC) plus **_loki** metadata.

from datetime import timedelta
from cabinet import Cabinet

cab = Cabinet()

lines = cab.log_query_loki(level="error", since=timedelta(days=7), limit=100)
rows = cab.log_query_documents_loki(since=timedelta(hours=24), limit=50)
issues = cab.log_query_issues_loki(since=timedelta(hours=24))

logdb

Removed in 3.0.0 — see Breaking changes in 3.0.0. Use **cab.log(...)**, **cab.log_query()** / **cab.log_query_issues()**, and the **cab.log_query*_loki()** helpers instead.

UI Module

The cabinet.ui submodule (src/cabinet/ui.py) provides interactive command-line components built on prompt_toolkit:

from cabinet.ui import list_selection, render_html, confirmation

# List selection
items = ["Option 1", "Option 2", "Option 3"]
selected_index = list_selection(items, "Choose an option:")

# HTML rendering
render_html("<b>Bold text</b> and <i>italic text</i>")

# Confirmation dialog
result = confirmation("Do you want to proceed?", "Confirmation")

Disclaimers

  • Although I've done quite a bit of testing, I can't guarantee everything that works on my machine will work on yours. Always back up your data to multiple places to avoid data loss.
  • If you find any issues, please contact me... or get your hands dirty and raise a PR!

Author

Project details


Release history Release notifications | RSS feed

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cabinet-1!3.0.0.tar.gz (51.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cabinet-1!3.0.0-py3-none-any.whl (40.5 kB view details)

Uploaded Python 3

File details

Details for the file cabinet-1!3.0.0.tar.gz.

File metadata

  • Download URL: cabinet-1!3.0.0.tar.gz
  • Upload date:
  • Size: 51.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for cabinet-1!3.0.0.tar.gz
Algorithm Hash digest
SHA256 cea36aabc0a1a47b59070161301016e331e745a3433e8c1f502dde5b29b867fd
MD5 768d3a603c9941771d6a987ad6d75dba
BLAKE2b-256 fa31ef34b4afb2e2c1063014aec733e03895ff274a2473751aedf9d4f9068be9

See more details on using hashes here.

File details

Details for the file cabinet-1!3.0.0-py3-none-any.whl.

File metadata

  • Download URL: cabinet-1!3.0.0-py3-none-any.whl
  • Upload date:
  • Size: 40.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for cabinet-1!3.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 041606cf044289cc3ca888414dfb44ce2e15db8a2950e3953c82e64ebac0ac2e
MD5 eaaf860c601628ae886ae7b64c968161
BLAKE2b-256 09a66cddd2cc86d8588b993b75761df16556842485556621060dc4dd9e9a7894

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page