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.** Usecab.log(...)instead. There is no separate database-only logging API. -
**--logdb/-ldbremoved.** Usecabinet -l/--logwith--levelas needed; the CLI follows the same rules as the Python API. -
File-first logging:
cab.log()/cabinet --logalways write to local-only daily files underpath_dir_logon 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()andcabinet --query:** Search log files underpath_dir_logonly. MongoDB is for Cabinet’s configuration/data collection, not logs. Optional**since** filters by UTC cutoff ortimedelta(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 underpath_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—usecabinet -e/--edit-fileor 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 changes in 3.0.0
- ✨ Features
- Minimal Installation and Setup
- Dependencies
- Structure
- CLI usage
- Configuration
- Examples
[put](#put)[get](#get)[remove](#remove)[export](#export)[append](#append)[edit](#edit)[edit_file](#edit_file)- Sending mail
[log](#log)[log_query](#log_query)[log_query_issues](#log_query_issues)- Loki queries
- UI Module
- Disclaimers
- Author
✨ Features
- Access your data across multiple projects
- Log messages to local files under
path_dir_log(and optional local*.jsonlfor 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--removeonly with MongoDB enabled - Send mail from the terminal (SMTP
email.portaccepts a number or numeric string;email.tomay be a string, comma-separated addresses, or a JSON array—same idea as--toon the CLI) - Library for interactive command-line interface components using
prompt_toolkit
Breaking change in 2.0.0
mongodb_connection_stringreplacesmongodb_usernameandmongodb_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.jsonor 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_logis 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_enabledis a boolean that determines whether MongoDB is used.mongodb_db_nameis the name of the database you want to use by default.mongodb_connection_stringis the connection string for MongoDB.editoris 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,
nanowill be used. You can change this withcabinet --configureand modifying theeditorattribute.
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 -lline goes to the classic**.log** format on local disk. - Do not share log directories: Set
path_dir_log/logging.log_dirto 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_logat something outside your Syncthing folders (for example~/.local/share/cabinet/logor~/.cache/cabinet/log).mkdir -p ~/.local/share/cabinet/logon each host before logging.
- Syncthing: Even if Cabinet’s config lives under a synced tree, point
- No log push HTTP:
cab.log()never POSTs to Loki; Promtail on each machine tails local*.jsonland pushes to Loki. Reads from Loki (cab.log_query*_loki) use HTTP only whenlogging.loki_urlis 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 includetimestamp,level,message,tags,source,hostnamefromsocket.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/lokidirectory, copypromtail.env.example→promtail.env, setLOKI_URL(reachable from that host) andCABINET_LOG_DIR(same absolute path aspath_dir_log/logging.log_dirin 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, includinghost.docker.internalvs LAN IP forLOKI_URL, is in../docker/loki/README.md(sibling of thiscabinetrepo when both live under the same parent directory).
- Central server (Loki + Grafana): One host runs the stack so all Promtail agents can push to it.
- 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_enabledtofalse, 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_urlto 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**(defaultcabinet, must match Promtail’sjoblabel) and**logging.loki_query_timeout**(seconds, default30). - 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 shoppingfrom the terminal- rather than
cabinet -ef "~/path/to/shopping_list.md"
- rather than
- or
cabinet.Cabinet().edit("shopping")- rather than
cabinet.Cabinet().edit("~/path/to/whatever.md")
- rather than
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 theemailobject 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-updateto 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.jsonand 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
cabinetcollection; 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cea36aabc0a1a47b59070161301016e331e745a3433e8c1f502dde5b29b867fd
|
|
| MD5 |
768d3a603c9941771d6a987ad6d75dba
|
|
| BLAKE2b-256 |
fa31ef34b4afb2e2c1063014aec733e03895ff274a2473751aedf9d4f9068be9
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
041606cf044289cc3ca888414dfb44ce2e15db8a2950e3953c82e64ebac0ac2e
|
|
| MD5 |
eaaf860c601628ae886ae7b64c968161
|
|
| BLAKE2b-256 |
09a66cddd2cc86d8588b993b75761df16556842485556621060dc4dd9e9a7894
|