mirror of
https://github.com/natelandau/obsidian-metadata.git
synced 2025-11-16 17:03:48 -05:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f431353e1 | ||
|
|
4e49445b08 | ||
|
|
5f9c79a9c1 | ||
|
|
34e7c07dd9 | ||
|
|
32a838c8e4 | ||
|
|
000ac1a16c | ||
|
|
1eb2d30d47 | ||
|
|
b6a3d115fd | ||
|
|
03e6ad59c4 | ||
|
|
0b744f65ee | ||
|
|
bf869cfc15 | ||
|
|
bd4b94aefa | ||
|
|
3932717c7e | ||
|
|
755151e2ed | ||
|
|
8f8174a902 | ||
|
|
3bbcf3a987 | ||
|
|
347dd4271f | ||
|
|
167997f527 | ||
|
|
0143967db8 | ||
|
|
446374b335 | ||
|
|
401d830942 | ||
|
|
7eb8ff5fa8 |
4
.github/workflows/automated-tests.yml
vendored
4
.github/workflows/automated-tests.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11"]
|
||||
steps:
|
||||
- uses: step-security/harden-runner@18bf8ad2ca49c14cbb28b91346d626ccfb00c518 # v2.1.0
|
||||
- uses: step-security/harden-runner@c8454efe5d0bdefd25384362fe217428ca277d57 # v2.2.0
|
||||
with:
|
||||
egress-policy: block
|
||||
disable-sudo: true
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
- name: Lint with Mypy
|
||||
run: poetry run mypy src/
|
||||
- name: lint with ruff
|
||||
run: poetry run ruff --extend-ignore=I001,D301,D401,PLR2004,PLR0913 src/
|
||||
run: poetry run ruff --extend-ignore=I001,D301,D401 src/
|
||||
- name: check pyproject.toml
|
||||
run: poetry run poetry check
|
||||
|
||||
|
||||
9
.github/workflows/commit-linter.yml
vendored
9
.github/workflows/commit-linter.yml
vendored
@@ -2,11 +2,14 @@
|
||||
name: Commit Linter
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened]
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
|
||||
permissions: # added using https://github.com/step-security/secure-workflows
|
||||
contents: read
|
||||
@@ -20,7 +23,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@18bf8ad2ca49c14cbb28b91346d626ccfb00c518 # v2.1.0
|
||||
uses: step-security/harden-runner@c8454efe5d0bdefd25384362fe217428ca277d57 # v2.2.0
|
||||
with:
|
||||
egress-policy: block
|
||||
allowed-endpoints: >
|
||||
|
||||
2
.github/workflows/create-release.yml
vendored
2
.github/workflows/create-release.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
matrix:
|
||||
python-version: ["3.11"]
|
||||
steps:
|
||||
- uses: step-security/harden-runner@18bf8ad2ca49c14cbb28b91346d626ccfb00c518 # v2.1.0
|
||||
- uses: step-security/harden-runner@c8454efe5d0bdefd25384362fe217428ca277d57 # v2.2.0
|
||||
with:
|
||||
egress-policy: block
|
||||
disable-sudo: true
|
||||
|
||||
13
.github/workflows/devcontainer-checker.yml
vendored
13
.github/workflows/devcontainer-checker.yml
vendored
@@ -3,14 +3,17 @@ name: "Dev Container Checker"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types: [opened, reopened]
|
||||
push:
|
||||
paths:
|
||||
- ".devcontainer/**"
|
||||
- ".github/workflows/devcontainer-checker.yml"
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
paths:
|
||||
- ".devcontainer/**"
|
||||
- ".github/workflows/devcontainer-checker.yml"
|
||||
@@ -24,7 +27,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: step-security/harden-runner@18bf8ad2ca49c14cbb28b91346d626ccfb00c518 # v2.1.0
|
||||
- uses: step-security/harden-runner@c8454efe5d0bdefd25384362fe217428ca277d57 # v2.2.0
|
||||
with:
|
||||
egress-policy: block
|
||||
allowed-endpoints: >
|
||||
@@ -55,7 +58,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build and run dev container task
|
||||
uses: devcontainers/ci@v0.2
|
||||
uses: devcontainers/ci@v0.3
|
||||
with:
|
||||
runCmd: |
|
||||
poe lint
|
||||
|
||||
2
.github/workflows/labeler.yml
vendored
2
.github/workflows/labeler.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@18bf8ad2ca49c14cbb28b91346d626ccfb00c518 # v2.1.0
|
||||
uses: step-security/harden-runner@c8454efe5d0bdefd25384362fe217428ca277d57 # v2.2.0
|
||||
with:
|
||||
egress-policy: block
|
||||
allowed-endpoints: >
|
||||
|
||||
2
.github/workflows/pr-linter.yml
vendored
2
.github/workflows/pr-linter.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@18bf8ad2ca49c14cbb28b91346d626ccfb00c518 # v2.1.0
|
||||
uses: step-security/harden-runner@c8454efe5d0bdefd25384362fe217428ca277d57 # v2.2.0
|
||||
with:
|
||||
egress-policy: block
|
||||
allowed-endpoints: >
|
||||
|
||||
2
.github/workflows/pypi-release.yml
vendored
2
.github/workflows/pypi-release.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
matrix:
|
||||
python-version: ["3.11"]
|
||||
steps:
|
||||
- uses: step-security/harden-runner@18bf8ad2ca49c14cbb28b91346d626ccfb00c518 # v2.1.0
|
||||
- uses: step-security/harden-runner@c8454efe5d0bdefd25384362fe217428ca277d57 # v2.2.0
|
||||
with:
|
||||
egress-policy: block
|
||||
disable-sudo: true
|
||||
|
||||
@@ -5,7 +5,7 @@ default_stages: [commit, manual]
|
||||
fail_fast: true
|
||||
repos:
|
||||
- repo: "https://github.com/commitizen-tools/commitizen"
|
||||
rev: v2.40.0
|
||||
rev: v2.42.1
|
||||
hooks:
|
||||
- id: commitizen
|
||||
- id: commitizen-branch
|
||||
@@ -61,10 +61,10 @@ repos:
|
||||
entry: yamllint --strict --config-file .yamllint.yml
|
||||
|
||||
- repo: "https://github.com/charliermarsh/ruff-pre-commit"
|
||||
rev: "v0.0.240"
|
||||
rev: "v0.0.254"
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: ["--extend-ignore", "I001,D301,D401,PLR2004,PLR0913"]
|
||||
args: ["--extend-ignore", "I001,D301,D401"]
|
||||
exclude: tests/
|
||||
|
||||
- repo: "https://github.com/jendrikseipp/vulture"
|
||||
|
||||
34
CHANGELOG.md
34
CHANGELOG.md
@@ -1,3 +1,37 @@
|
||||
## v0.7.0 (2023-03-11)
|
||||
|
||||
### Feat
|
||||
|
||||
- transpose metadata between frontmatter and inline
|
||||
- select insert location for new inline metadata
|
||||
|
||||
### Fix
|
||||
|
||||
- exit after committing changes
|
||||
- fix typo and sort order of options
|
||||
|
||||
## v0.6.1 (2023-03-03)
|
||||
|
||||
### Fix
|
||||
|
||||
- improve error handling when frontmatter malformed
|
||||
|
||||
### Refactor
|
||||
|
||||
- use single console instance
|
||||
|
||||
## v0.6.0 (2023-02-06)
|
||||
|
||||
### Feat
|
||||
|
||||
- transpose metadata (#18)
|
||||
|
||||
### Fix
|
||||
|
||||
- **ui**: add seperator to top of select lists
|
||||
- allow adding inline tags with same key different values (#17)
|
||||
- remove unnecessary question when viewing diffs
|
||||
|
||||
## v0.5.0 (2023-02-04)
|
||||
|
||||
### Feat
|
||||
|
||||
14
README.md
14
README.md
@@ -5,7 +5,7 @@
|
||||
A script to make batch updates to metadata in an Obsidian vault. No changes are
|
||||
made to the Vault until they are explicitly committed.
|
||||
|
||||
[](https://asciinema.org/a/555789)
|
||||
[](https://asciinema.org/a/DQk0ufza1azwU3QFkE6XV33nm)
|
||||
|
||||
## Important Disclaimer
|
||||
|
||||
@@ -60,6 +60,7 @@ Once installed, run `obsidian-metadata` in your terminal to enter an interactive
|
||||
- **List notes in scope**: List notes that will be processed.
|
||||
|
||||
**Add Metadata**: Add new metadata to your vault.
|
||||
When adding a new key to inline metadata, the `insert location` value in the config file will specify where in the note it will be inserted.
|
||||
|
||||
- **Add new metadata to the frontmatter**
|
||||
- **Add new inline metadata** - Set `insert_location` in the config to control where the new metadata is inserted. (Default: Bottom)
|
||||
@@ -77,6 +78,13 @@ Once installed, run `obsidian-metadata` in your terminal to enter an interactive
|
||||
- **Delete a value from a key**
|
||||
- **Delete an inline tag**
|
||||
|
||||
**Transpose Metadata**: Move metadata from inline to frontmatter or the reverse.
|
||||
When transposing to inline metadata, the `insert location` value in the config file will specify where in the note it will be inserted.
|
||||
|
||||
- **Transpose all metadata** - Moves all frontmatter to inline metadata, or the reverse
|
||||
- **Transpose key** - Transposes a specific key and all it's values
|
||||
- **Transpose value**- Transpose a specific key:value pair
|
||||
|
||||
**Review Changes**: Prior to committing changes, review all changes that will be made.
|
||||
|
||||
- **View a diff of the changes** that will be made
|
||||
@@ -104,7 +112,7 @@ Below is an example with two vaults.
|
||||
|
||||
# Location to add metadata. One of:
|
||||
# TOP: Directly after frontmatter.
|
||||
# AFTER_TITLE: After a header following frontmatter.
|
||||
# AFTER_TITLE: After the first header following frontmatter.
|
||||
# BOTTOM: The bottom of the note
|
||||
insert_location = "BOTTOM"
|
||||
|
||||
@@ -122,7 +130,7 @@ To bypass the configuration file and specify a vault to use at runtime use the `
|
||||
|
||||
There are two ways to contribute to this project.
|
||||
|
||||
### 1. Containerized development (Recommended)
|
||||
### 1. Containerized development
|
||||
|
||||
1. Clone this repository. `git clone https://github.com/natelandau/obsidian-metadata`
|
||||
2. Open the repository in Visual Studio Code
|
||||
|
||||
@@ -7,8 +7,8 @@ coverage:
|
||||
threshold: 5% # the leniency in hitting the target
|
||||
patch:
|
||||
default:
|
||||
target: 50%
|
||||
threshold: 5%
|
||||
target: 50%
|
||||
threshold: 5%
|
||||
ignore:
|
||||
- tests/
|
||||
|
||||
|
||||
414
poetry.lock
generated
414
poetry.lock
generated
@@ -1,31 +1,20 @@
|
||||
# This file is automatically @generated by Poetry and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "absolufy-imports"
|
||||
version = "0.3.1"
|
||||
description = "A tool to automatically replace relative imports with absolute ones."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6.1"
|
||||
files = [
|
||||
{file = "absolufy_imports-0.3.1-py2.py3-none-any.whl", hash = "sha256:49bf7c753a9282006d553ba99217f48f947e3eef09e18a700f8a82f75dc7fc5c"},
|
||||
{file = "absolufy_imports-0.3.1.tar.gz", hash = "sha256:c90638a6c0b66826d1fb4880ddc20ef7701af34192c94faf40b95d32b59f9793"},
|
||||
]
|
||||
# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "argcomplete"
|
||||
version = "2.0.0"
|
||||
version = "2.0.6"
|
||||
description = "Bash tab completion for argparse"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "argcomplete-2.0.0-py2.py3-none-any.whl", hash = "sha256:cffa11ea77999bb0dd27bb25ff6dc142a6796142f68d45b1a26b11f58724561e"},
|
||||
{file = "argcomplete-2.0.0.tar.gz", hash = "sha256:6372ad78c89d662035101418ae253668445b391755cfe94ea52f1b9d22425b20"},
|
||||
{file = "argcomplete-2.0.6-py3-none-any.whl", hash = "sha256:6c2170b3e0ab54683cb28d319b65261bde1f11388be688b68118b7d281e34c94"},
|
||||
{file = "argcomplete-2.0.6.tar.gz", hash = "sha256:dc33528d96727882b576b24bc89ed038f3c6abbb6855ff9bb6be23384afff9d6"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
test = ["coverage", "flake8", "pexpect", "wheel"]
|
||||
lint = ["flake8", "mypy"]
|
||||
test = ["coverage", "flake8", "mypy", "pexpect", "wheel"]
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
@@ -151,14 +140,14 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "commitizen"
|
||||
version = "2.40.0"
|
||||
version = "2.42.1"
|
||||
description = "Python commitizen client tool"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6.2,<4.0.0"
|
||||
files = [
|
||||
{file = "commitizen-2.40.0-py3-none-any.whl", hash = "sha256:44b589869529c297d4ef594bb7560388d3367b3ae8af36b0664d2f51a28e8f87"},
|
||||
{file = "commitizen-2.40.0.tar.gz", hash = "sha256:8f1a09589ffb87bb17df17261423e88299bd63432dbfc4e6fc6657fea23dddc0"},
|
||||
{file = "commitizen-2.42.1-py3-none-any.whl", hash = "sha256:fad7d37cfae361a859b713d4ac591859d5ca03137dd52de4e1bd208f7f45d5dc"},
|
||||
{file = "commitizen-2.42.1.tar.gz", hash = "sha256:eac18c7c65587061aac6829534907aeb208405b8230bfd35ec08503c228a7f17"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -176,63 +165,63 @@ typing-extensions = ">=4.0.1,<5.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.1.0"
|
||||
version = "7.2.1"
|
||||
description = "Code coverage measurement for Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "coverage-7.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3b946bbcd5a8231383450b195cfb58cb01cbe7f8949f5758566b881df4b33baf"},
|
||||
{file = "coverage-7.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec8e767f13be637d056f7e07e61d089e555f719b387a7070154ad80a0ff31801"},
|
||||
{file = "coverage-7.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a5a5879a939cb84959d86869132b00176197ca561c664fc21478c1eee60d75"},
|
||||
{file = "coverage-7.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b643cb30821e7570c0aaf54feaf0bfb630b79059f85741843e9dc23f33aaca2c"},
|
||||
{file = "coverage-7.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32df215215f3af2c1617a55dbdfb403b772d463d54d219985ac7cd3bf124cada"},
|
||||
{file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:33d1ae9d4079e05ac4cc1ef9e20c648f5afabf1a92adfaf2ccf509c50b85717f"},
|
||||
{file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:29571503c37f2ef2138a306d23e7270687c0efb9cab4bd8038d609b5c2393a3a"},
|
||||
{file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:63ffd21aa133ff48c4dff7adcc46b7ec8b565491bfc371212122dd999812ea1c"},
|
||||
{file = "coverage-7.1.0-cp310-cp310-win32.whl", hash = "sha256:4b14d5e09c656de5038a3f9bfe5228f53439282abcab87317c9f7f1acb280352"},
|
||||
{file = "coverage-7.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:8361be1c2c073919500b6601220a6f2f98ea0b6d2fec5014c1d9cfa23dd07038"},
|
||||
{file = "coverage-7.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:da9b41d4539eefd408c46725fb76ecba3a50a3367cafb7dea5f250d0653c1040"},
|
||||
{file = "coverage-7.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5b15ed7644ae4bee0ecf74fee95808dcc34ba6ace87e8dfbf5cb0dc20eab45a"},
|
||||
{file = "coverage-7.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d12d076582507ea460ea2a89a8c85cb558f83406c8a41dd641d7be9a32e1274f"},
|
||||
{file = "coverage-7.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2617759031dae1bf183c16cef8fcfb3de7617f394c813fa5e8e46e9b82d4222"},
|
||||
{file = "coverage-7.1.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4e4881fa9e9667afcc742f0c244d9364d197490fbc91d12ac3b5de0bf2df146"},
|
||||
{file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9d58885215094ab4a86a6aef044e42994a2bd76a446dc59b352622655ba6621b"},
|
||||
{file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ffeeb38ee4a80a30a6877c5c4c359e5498eec095878f1581453202bfacc8fbc2"},
|
||||
{file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3baf5f126f30781b5e93dbefcc8271cb2491647f8283f20ac54d12161dff080e"},
|
||||
{file = "coverage-7.1.0-cp311-cp311-win32.whl", hash = "sha256:ded59300d6330be27bc6cf0b74b89ada58069ced87c48eaf9344e5e84b0072f7"},
|
||||
{file = "coverage-7.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:6a43c7823cd7427b4ed763aa7fb63901ca8288591323b58c9cd6ec31ad910f3c"},
|
||||
{file = "coverage-7.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7a726d742816cb3a8973c8c9a97539c734b3a309345236cd533c4883dda05b8d"},
|
||||
{file = "coverage-7.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc7c85a150501286f8b56bd8ed3aa4093f4b88fb68c0843d21ff9656f0009d6a"},
|
||||
{file = "coverage-7.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5b4198d85a3755d27e64c52f8c95d6333119e49fd001ae5798dac872c95e0f8"},
|
||||
{file = "coverage-7.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddb726cb861c3117a553f940372a495fe1078249ff5f8a5478c0576c7be12050"},
|
||||
{file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:51b236e764840a6df0661b67e50697aaa0e7d4124ca95e5058fa3d7cbc240b7c"},
|
||||
{file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7ee5c9bb51695f80878faaa5598040dd6c9e172ddcf490382e8aedb8ec3fec8d"},
|
||||
{file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c31b75ae466c053a98bf26843563b3b3517b8f37da4d47b1c582fdc703112bc3"},
|
||||
{file = "coverage-7.1.0-cp37-cp37m-win32.whl", hash = "sha256:3b155caf3760408d1cb903b21e6a97ad4e2bdad43cbc265e3ce0afb8e0057e73"},
|
||||
{file = "coverage-7.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2a60d6513781e87047c3e630b33b4d1e89f39836dac6e069ffee28c4786715f5"},
|
||||
{file = "coverage-7.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f2cba5c6db29ce991029b5e4ac51eb36774458f0a3b8d3137241b32d1bb91f06"},
|
||||
{file = "coverage-7.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:beeb129cacea34490ffd4d6153af70509aa3cda20fdda2ea1a2be870dfec8d52"},
|
||||
{file = "coverage-7.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c45948f613d5d18c9ec5eaa203ce06a653334cf1bd47c783a12d0dd4fd9c851"},
|
||||
{file = "coverage-7.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef382417db92ba23dfb5864a3fc9be27ea4894e86620d342a116b243ade5d35d"},
|
||||
{file = "coverage-7.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c7c0d0827e853315c9bbd43c1162c006dd808dbbe297db7ae66cd17b07830f0"},
|
||||
{file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e5cdbb5cafcedea04924568d990e20ce7f1945a1dd54b560f879ee2d57226912"},
|
||||
{file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9817733f0d3ea91bea80de0f79ef971ae94f81ca52f9b66500c6a2fea8e4b4f8"},
|
||||
{file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:218fe982371ac7387304153ecd51205f14e9d731b34fb0568181abaf7b443ba0"},
|
||||
{file = "coverage-7.1.0-cp38-cp38-win32.whl", hash = "sha256:04481245ef966fbd24ae9b9e537ce899ae584d521dfbe78f89cad003c38ca2ab"},
|
||||
{file = "coverage-7.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:8ae125d1134bf236acba8b83e74c603d1b30e207266121e76484562bc816344c"},
|
||||
{file = "coverage-7.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2bf1d5f2084c3932b56b962a683074a3692bce7cabd3aa023c987a2a8e7612f6"},
|
||||
{file = "coverage-7.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:98b85dd86514d889a2e3dd22ab3c18c9d0019e696478391d86708b805f4ea0fa"},
|
||||
{file = "coverage-7.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38da2db80cc505a611938d8624801158e409928b136c8916cd2e203970dde4dc"},
|
||||
{file = "coverage-7.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3164d31078fa9efe406e198aecd2a02d32a62fecbdef74f76dad6a46c7e48311"},
|
||||
{file = "coverage-7.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db61a79c07331e88b9a9974815c075fbd812bc9dbc4dc44b366b5368a2936063"},
|
||||
{file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ccb092c9ede70b2517a57382a601619d20981f56f440eae7e4d7eaafd1d1d09"},
|
||||
{file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:33ff26d0f6cc3ca8de13d14fde1ff8efe1456b53e3f0273e63cc8b3c84a063d8"},
|
||||
{file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d47dd659a4ee952e90dc56c97d78132573dc5c7b09d61b416a9deef4ebe01a0c"},
|
||||
{file = "coverage-7.1.0-cp39-cp39-win32.whl", hash = "sha256:d248cd4a92065a4d4543b8331660121b31c4148dd00a691bfb7a5cdc7483cfa4"},
|
||||
{file = "coverage-7.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:7ed681b0f8e8bcbbffa58ba26fcf5dbc8f79e7997595bf071ed5430d8c08d6f3"},
|
||||
{file = "coverage-7.1.0-pp37.pp38.pp39-none-any.whl", hash = "sha256:755e89e32376c850f826c425ece2c35a4fc266c081490eb0a841e7c1cb0d3bda"},
|
||||
{file = "coverage-7.1.0.tar.gz", hash = "sha256:10188fe543560ec4874f974b5305cd1a8bdcfa885ee00ea3a03733464c4ca265"},
|
||||
{file = "coverage-7.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:49567ec91fc5e0b15356da07a2feabb421d62f52a9fff4b1ec40e9e19772f5f8"},
|
||||
{file = "coverage-7.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d2ef6cae70168815ed91388948b5f4fcc69681480a0061114db737f957719f03"},
|
||||
{file = "coverage-7.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3004765bca3acd9e015794e5c2f0c9a05587f5e698127ff95e9cfba0d3f29339"},
|
||||
{file = "coverage-7.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cca7c0b7f5881dfe0291ef09ba7bb1582cb92ab0aeffd8afb00c700bf692415a"},
|
||||
{file = "coverage-7.2.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2167d116309f564af56f9aa5e75ef710ef871c5f9b313a83050035097b56820"},
|
||||
{file = "coverage-7.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cb5f152fb14857cbe7f3e8c9a5d98979c4c66319a33cad6e617f0067c9accdc4"},
|
||||
{file = "coverage-7.2.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:87dc37f16fb5e3a28429e094145bf7c1753e32bb50f662722e378c5851f7fdc6"},
|
||||
{file = "coverage-7.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e191a63a05851f8bce77bc875e75457f9b01d42843f8bd7feed2fc26bbe60833"},
|
||||
{file = "coverage-7.2.1-cp310-cp310-win32.whl", hash = "sha256:e3ea04b23b114572b98a88c85379e9e9ae031272ba1fb9b532aa934c621626d4"},
|
||||
{file = "coverage-7.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:0cf557827be7eca1c38a2480484d706693e7bb1929e129785fe59ec155a59de6"},
|
||||
{file = "coverage-7.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:570c21a29493b350f591a4b04c158ce1601e8d18bdcd21db136fbb135d75efa6"},
|
||||
{file = "coverage-7.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9e872b082b32065ac2834149dc0adc2a2e6d8203080501e1e3c3c77851b466f9"},
|
||||
{file = "coverage-7.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fac6343bae03b176e9b58104a9810df3cdccd5cfed19f99adfa807ffbf43cf9b"},
|
||||
{file = "coverage-7.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abacd0a738e71b20e224861bc87e819ef46fedba2fb01bc1af83dfd122e9c319"},
|
||||
{file = "coverage-7.2.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9256d4c60c4bbfec92721b51579c50f9e5062c21c12bec56b55292464873508"},
|
||||
{file = "coverage-7.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:80559eaf6c15ce3da10edb7977a1548b393db36cbc6cf417633eca05d84dd1ed"},
|
||||
{file = "coverage-7.2.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0bd7e628f6c3ec4e7d2d24ec0e50aae4e5ae95ea644e849d92ae4805650b4c4e"},
|
||||
{file = "coverage-7.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09643fb0df8e29f7417adc3f40aaf379d071ee8f0350ab290517c7004f05360b"},
|
||||
{file = "coverage-7.2.1-cp311-cp311-win32.whl", hash = "sha256:1b7fb13850ecb29b62a447ac3516c777b0e7a09ecb0f4bb6718a8654c87dfc80"},
|
||||
{file = "coverage-7.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:617a94ada56bbfe547aa8d1b1a2b8299e2ec1ba14aac1d4b26a9f7d6158e1273"},
|
||||
{file = "coverage-7.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8649371570551d2fd7dee22cfbf0b61f1747cdfb2b7587bb551e4beaaa44cb97"},
|
||||
{file = "coverage-7.2.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d2b9b5e70a21474c105a133ba227c61bc95f2ac3b66861143ce39a5ea4b3f84"},
|
||||
{file = "coverage-7.2.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae82c988954722fa07ec5045c57b6d55bc1a0890defb57cf4a712ced65b26ddd"},
|
||||
{file = "coverage-7.2.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:861cc85dfbf55a7a768443d90a07e0ac5207704a9f97a8eb753292a7fcbdfcfc"},
|
||||
{file = "coverage-7.2.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0339dc3237c0d31c3b574f19c57985fcbe494280153bbcad33f2cdf469f4ac3e"},
|
||||
{file = "coverage-7.2.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:5928b85416a388dd557ddc006425b0c37e8468bd1c3dc118c1a3de42f59e2a54"},
|
||||
{file = "coverage-7.2.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8d3843ca645f62c426c3d272902b9de90558e9886f15ddf5efe757b12dd376f5"},
|
||||
{file = "coverage-7.2.1-cp37-cp37m-win32.whl", hash = "sha256:6a034480e9ebd4e83d1aa0453fd78986414b5d237aea89a8fdc35d330aa13bae"},
|
||||
{file = "coverage-7.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6fce673f79a0e017a4dc35e18dc7bb90bf6d307c67a11ad5e61ca8d42b87cbff"},
|
||||
{file = "coverage-7.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7f099da6958ddfa2ed84bddea7515cb248583292e16bb9231d151cd528eab657"},
|
||||
{file = "coverage-7.2.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:97a3189e019d27e914ecf5c5247ea9f13261d22c3bb0cfcfd2a9b179bb36f8b1"},
|
||||
{file = "coverage-7.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a81dbcf6c6c877986083d00b834ac1e84b375220207a059ad45d12f6e518a4e3"},
|
||||
{file = "coverage-7.2.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2c3dde4c0b9be4b02067185136b7ee4681978228ad5ec1278fa74f5ca3e99"},
|
||||
{file = "coverage-7.2.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a209d512d157379cc9ab697cbdbb4cfd18daa3e7eebaa84c3d20b6af0037384"},
|
||||
{file = "coverage-7.2.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f3d07edb912a978915576a776756069dede66d012baa503022d3a0adba1b6afa"},
|
||||
{file = "coverage-7.2.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8dca3c1706670297851bca1acff9618455122246bdae623be31eca744ade05ec"},
|
||||
{file = "coverage-7.2.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b1991a6d64231a3e5bbe3099fb0dd7c9aeaa4275ad0e0aeff4cb9ef885c62ba2"},
|
||||
{file = "coverage-7.2.1-cp38-cp38-win32.whl", hash = "sha256:22c308bc508372576ffa3d2dbc4824bb70d28eeb4fcd79d4d1aed663a06630d0"},
|
||||
{file = "coverage-7.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:b0c0d46de5dd97f6c2d1b560bf0fcf0215658097b604f1840365296302a9d1fb"},
|
||||
{file = "coverage-7.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4dd34a935de268a133e4741827ae951283a28c0125ddcdbcbba41c4b98f2dfef"},
|
||||
{file = "coverage-7.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0f8318ed0f3c376cfad8d3520f496946977abde080439d6689d7799791457454"},
|
||||
{file = "coverage-7.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:834c2172edff5a08d78e2f53cf5e7164aacabeb66b369f76e7bb367ca4e2d993"},
|
||||
{file = "coverage-7.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4d70c853f0546855f027890b77854508bdb4d6a81242a9d804482e667fff6e6"},
|
||||
{file = "coverage-7.2.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a6450da4c7afc4534305b2b7d8650131e130610cea448ff240b6ab73d7eab63"},
|
||||
{file = "coverage-7.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:99f4dd81b2bb8fc67c3da68b1f5ee1650aca06faa585cbc6818dbf67893c6d58"},
|
||||
{file = "coverage-7.2.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bdd3f2f285ddcf2e75174248b2406189261a79e7fedee2ceeadc76219b6faa0e"},
|
||||
{file = "coverage-7.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f29351393eb05e6326f044a7b45ed8e38cb4dcc38570d12791f271399dc41431"},
|
||||
{file = "coverage-7.2.1-cp39-cp39-win32.whl", hash = "sha256:e2b50ebc2b6121edf352336d503357321b9d8738bb7a72d06fc56153fd3f4cd8"},
|
||||
{file = "coverage-7.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:bd5a12239c0006252244f94863f1c518ac256160cd316ea5c47fb1a11b25889a"},
|
||||
{file = "coverage-7.2.1-pp37.pp38.pp39-none-any.whl", hash = "sha256:436313d129db7cf5b4ac355dd2bd3f7c7e5294af077b090b85de75f8458b8616"},
|
||||
{file = "coverage-7.2.1.tar.gz", hash = "sha256:c77f2a9093ccf329dd523a9b2b3c854c20d2a3d968b6def3b820272ca6732242"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -308,33 +297,16 @@ files = [
|
||||
docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"]
|
||||
testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "flake8"
|
||||
version = "6.0.0"
|
||||
description = "the modular source code checker: pep8 pyflakes and co"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.8.1"
|
||||
files = [
|
||||
{file = "flake8-6.0.0-py2.py3-none-any.whl", hash = "sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7"},
|
||||
{file = "flake8-6.0.0.tar.gz", hash = "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
mccabe = ">=0.7.0,<0.8.0"
|
||||
pycodestyle = ">=2.10.0,<2.11.0"
|
||||
pyflakes = ">=3.0.0,<3.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "identify"
|
||||
version = "2.5.17"
|
||||
version = "2.5.19"
|
||||
description = "File identification library for Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "identify-2.5.17-py2.py3-none-any.whl", hash = "sha256:7d526dd1283555aafcc91539acc061d8f6f59adb0a7bba462735b0a318bff7ed"},
|
||||
{file = "identify-2.5.17.tar.gz", hash = "sha256:93cc61a861052de9d4c541a7acb7e3dcc9c11b398a2144f6e52ae5285f5f4f06"},
|
||||
{file = "identify-2.5.19-py2.py3-none-any.whl", hash = "sha256:3ee3533e7f6f5023157fbebbd5687bb4b698ce6f305259e0d24b2d7d9efb72bc"},
|
||||
{file = "identify-2.5.19.tar.gz", hash = "sha256:4102ecd051f6884449e7359e55b38ba6cd7aafb6ef27b8e2b38495a5723ea106"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -417,24 +389,24 @@ dev = ["Sphinx (>=4.1.1)", "black (>=19.10b0)", "colorama (>=0.3.4)", "docutils
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "2.1.0"
|
||||
version = "2.2.0"
|
||||
description = "Python port of markdown-it. Markdown parsing, done right!"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "markdown-it-py-2.1.0.tar.gz", hash = "sha256:cf7e59fed14b5ae17c0006eff14a2d9a00ed5f3a846148153899a0224e2c07da"},
|
||||
{file = "markdown_it_py-2.1.0-py3-none-any.whl", hash = "sha256:93de681e5c021a432c63147656fe21790bc01231e0cd2da73626f1aa3ac0fe27"},
|
||||
{file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"},
|
||||
{file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
mdurl = ">=0.1,<1.0"
|
||||
|
||||
[package.extras]
|
||||
benchmarking = ["psutil", "pytest", "pytest-benchmark (>=3.2,<4.0)"]
|
||||
code-style = ["pre-commit (==2.6)"]
|
||||
compare = ["commonmark (>=0.9.1,<0.10.0)", "markdown (>=3.3.6,<3.4.0)", "mistletoe (>=0.8.1,<0.9.0)", "mistune (>=2.0.2,<2.1.0)", "panflute (>=2.1.3,<2.2.0)"]
|
||||
linkify = ["linkify-it-py (>=1.0,<2.0)"]
|
||||
benchmarking = ["psutil", "pytest", "pytest-benchmark"]
|
||||
code-style = ["pre-commit (>=3.0,<4.0)"]
|
||||
compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
|
||||
linkify = ["linkify-it-py (>=1,<3)"]
|
||||
plugins = ["mdit-py-plugins"]
|
||||
profiling = ["gprof2dot"]
|
||||
rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
|
||||
@@ -500,18 +472,6 @@ files = [
|
||||
{file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mccabe"
|
||||
version = "0.7.0"
|
||||
description = "McCabe checker, plugin for flake8"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
|
||||
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
@@ -526,46 +486,42 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
version = "0.991"
|
||||
version = "1.1.1"
|
||||
description = "Optional static typing for Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "mypy-0.991-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab"},
|
||||
{file = "mypy-0.991-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d"},
|
||||
{file = "mypy-0.991-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c8f3be99e8a8bd403caa8c03be619544bc2c77a7093685dcf308c6b109426c6"},
|
||||
{file = "mypy-0.991-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc9ec663ed6c8f15f4ae9d3c04c989b744436c16d26580eaa760ae9dd5d662eb"},
|
||||
{file = "mypy-0.991-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4307270436fd7694b41f913eb09210faff27ea4979ecbcd849e57d2da2f65305"},
|
||||
{file = "mypy-0.991-cp310-cp310-win_amd64.whl", hash = "sha256:901c2c269c616e6cb0998b33d4adbb4a6af0ac4ce5cd078afd7bc95830e62c1c"},
|
||||
{file = "mypy-0.991-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d13674f3fb73805ba0c45eb6c0c3053d218aa1f7abead6e446d474529aafc372"},
|
||||
{file = "mypy-0.991-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c8cd4fb70e8584ca1ed5805cbc7c017a3d1a29fb450621089ffed3e99d1857f"},
|
||||
{file = "mypy-0.991-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:209ee89fbb0deed518605edddd234af80506aec932ad28d73c08f1400ef80a33"},
|
||||
{file = "mypy-0.991-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37bd02ebf9d10e05b00d71302d2c2e6ca333e6c2a8584a98c00e038db8121f05"},
|
||||
{file = "mypy-0.991-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:26efb2fcc6b67e4d5a55561f39176821d2adf88f2745ddc72751b7890f3194ad"},
|
||||
{file = "mypy-0.991-cp311-cp311-win_amd64.whl", hash = "sha256:3a700330b567114b673cf8ee7388e949f843b356a73b5ab22dd7cff4742a5297"},
|
||||
{file = "mypy-0.991-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1f7d1a520373e2272b10796c3ff721ea1a0712288cafaa95931e66aa15798813"},
|
||||
{file = "mypy-0.991-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:641411733b127c3e0dab94c45af15fea99e4468f99ac88b39efb1ad677da5711"},
|
||||
{file = "mypy-0.991-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3d80e36b7d7a9259b740be6d8d906221789b0d836201af4234093cae89ced0cd"},
|
||||
{file = "mypy-0.991-cp37-cp37m-win_amd64.whl", hash = "sha256:e62ebaad93be3ad1a828a11e90f0e76f15449371ffeecca4a0a0b9adc99abcef"},
|
||||
{file = "mypy-0.991-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b86ce2c1866a748c0f6faca5232059f881cda6dda2a893b9a8373353cfe3715a"},
|
||||
{file = "mypy-0.991-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac6e503823143464538efda0e8e356d871557ef60ccd38f8824a4257acc18d93"},
|
||||
{file = "mypy-0.991-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0cca5adf694af539aeaa6ac633a7afe9bbd760df9d31be55ab780b77ab5ae8bf"},
|
||||
{file = "mypy-0.991-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12c56bf73cdab116df96e4ff39610b92a348cc99a1307e1da3c3768bbb5b135"},
|
||||
{file = "mypy-0.991-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:652b651d42f155033a1967739788c436491b577b6a44e4c39fb340d0ee7f0d70"},
|
||||
{file = "mypy-0.991-cp38-cp38-win_amd64.whl", hash = "sha256:4175593dc25d9da12f7de8de873a33f9b2b8bdb4e827a7cae952e5b1a342e243"},
|
||||
{file = "mypy-0.991-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:98e781cd35c0acf33eb0295e8b9c55cdbef64fcb35f6d3aa2186f289bed6e80d"},
|
||||
{file = "mypy-0.991-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6d7464bac72a85cb3491c7e92b5b62f3dcccb8af26826257760a552a5e244aa5"},
|
||||
{file = "mypy-0.991-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c9166b3f81a10cdf9b49f2d594b21b31adadb3d5e9db9b834866c3258b695be3"},
|
||||
{file = "mypy-0.991-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8472f736a5bfb159a5e36740847808f6f5b659960115ff29c7cecec1741c648"},
|
||||
{file = "mypy-0.991-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e80e758243b97b618cdf22004beb09e8a2de1af481382e4d84bc52152d1c476"},
|
||||
{file = "mypy-0.991-cp39-cp39-win_amd64.whl", hash = "sha256:74e259b5c19f70d35fcc1ad3d56499065c601dfe94ff67ae48b85596b9ec1461"},
|
||||
{file = "mypy-0.991-py3-none-any.whl", hash = "sha256:de32edc9b0a7e67c2775e574cb061a537660e51210fbf6006b0b36ea695ae9bb"},
|
||||
{file = "mypy-0.991.tar.gz", hash = "sha256:3c0165ba8f354a6d9881809ef29f1a9318a236a6d81c690094c5df32107bde06"},
|
||||
{file = "mypy-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39c7119335be05630611ee798cc982623b9e8f0cff04a0b48dfc26100e0b97af"},
|
||||
{file = "mypy-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:61bf08362e93b6b12fad3eab68c4ea903a077b87c90ac06c11e3d7a09b56b9c1"},
|
||||
{file = "mypy-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbb19c9f662e41e474e0cff502b7064a7edc6764f5262b6cd91d698163196799"},
|
||||
{file = "mypy-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:315ac73cc1cce4771c27d426b7ea558fb4e2836f89cb0296cbe056894e3a1f78"},
|
||||
{file = "mypy-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5cb14ff9919b7df3538590fc4d4c49a0f84392237cbf5f7a816b4161c061829e"},
|
||||
{file = "mypy-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:26cdd6a22b9b40b2fd71881a8a4f34b4d7914c679f154f43385ca878a8297389"},
|
||||
{file = "mypy-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b5f81b40d94c785f288948c16e1f2da37203c6006546c5d947aab6f90aefef2"},
|
||||
{file = "mypy-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21b437be1c02712a605591e1ed1d858aba681757a1e55fe678a15c2244cd68a5"},
|
||||
{file = "mypy-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d809f88734f44a0d44959d795b1e6f64b2bbe0ea4d9cc4776aa588bb4229fc1c"},
|
||||
{file = "mypy-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:a380c041db500e1410bb5b16b3c1c35e61e773a5c3517926b81dfdab7582be54"},
|
||||
{file = "mypy-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b7c7b708fe9a871a96626d61912e3f4ddd365bf7f39128362bc50cbd74a634d5"},
|
||||
{file = "mypy-1.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1c10fa12df1232c936830839e2e935d090fc9ee315744ac33b8a32216b93707"},
|
||||
{file = "mypy-1.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0a28a76785bf57655a8ea5eb0540a15b0e781c807b5aa798bd463779988fa1d5"},
|
||||
{file = "mypy-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ef6a01e563ec6a4940784c574d33f6ac1943864634517984471642908b30b6f7"},
|
||||
{file = "mypy-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d64c28e03ce40d5303450f547e07418c64c241669ab20610f273c9e6290b4b0b"},
|
||||
{file = "mypy-1.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64cc3afb3e9e71a79d06e3ed24bb508a6d66f782aff7e56f628bf35ba2e0ba51"},
|
||||
{file = "mypy-1.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce61663faf7a8e5ec6f456857bfbcec2901fbdb3ad958b778403f63b9e606a1b"},
|
||||
{file = "mypy-1.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2b0c373d071593deefbcdd87ec8db91ea13bd8f1328d44947e88beae21e8d5e9"},
|
||||
{file = "mypy-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:2888ce4fe5aae5a673386fa232473014056967f3904f5abfcf6367b5af1f612a"},
|
||||
{file = "mypy-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:19ba15f9627a5723e522d007fe708007bae52b93faab00f95d72f03e1afa9598"},
|
||||
{file = "mypy-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:59bbd71e5c58eed2e992ce6523180e03c221dcd92b52f0e792f291d67b15a71c"},
|
||||
{file = "mypy-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9401e33814cec6aec8c03a9548e9385e0e228fc1b8b0a37b9ea21038e64cdd8a"},
|
||||
{file = "mypy-1.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4b398d8b1f4fba0e3c6463e02f8ad3346f71956b92287af22c9b12c3ec965a9f"},
|
||||
{file = "mypy-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:69b35d1dcb5707382810765ed34da9db47e7f95b3528334a3c999b0c90fe523f"},
|
||||
{file = "mypy-1.1.1-py3-none-any.whl", hash = "sha256:4e4e8b362cdf99ba00c2b218036002bdcdf1e0de085cdb296a49df03fb31dfc4"},
|
||||
{file = "mypy-1.1.1.tar.gz", hash = "sha256:ae9ceae0f5b9059f33dbc62dea087e942c0ccab4b7a003719cb70f9b8abfa32f"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
mypy-extensions = ">=0.4.3"
|
||||
mypy-extensions = ">=1.0.0"
|
||||
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
|
||||
typing-extensions = ">=3.10"
|
||||
|
||||
@@ -640,14 +596,14 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pdoc"
|
||||
version = "12.3.1"
|
||||
version = "13.0.0"
|
||||
description = "API Documentation for Python Projects"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pdoc-12.3.1-py3-none-any.whl", hash = "sha256:c3f24f31286e634de9c76fa6e67bd5c0c5e74360b41dc91e6b82499831eb52d8"},
|
||||
{file = "pdoc-12.3.1.tar.gz", hash = "sha256:453236f225feddb8a9071428f1982a78d74b9b3da4bc4433aedb64dbd0cc87ab"},
|
||||
{file = "pdoc-13.0.0-py3-none-any.whl", hash = "sha256:f9088b1c10f3296f46a08796e05e307470af5f4253f71d536781f6c305baf912"},
|
||||
{file = "pdoc-13.0.0.tar.gz", hash = "sha256:aadbf6c757c6e65c4754d6c26c4eb6c1bf8c7a9fb893f1fbe5a7b879dde59e46"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -656,38 +612,23 @@ MarkupSafe = "*"
|
||||
pygments = ">=2.12.0"
|
||||
|
||||
[package.extras]
|
||||
dev = ["black", "hypothesis", "mypy", "pytest", "pytest-cov", "pytest-timeout", "ruff", "tox", "types-pygments"]
|
||||
|
||||
[[package]]
|
||||
name = "pep8-naming"
|
||||
version = "0.13.3"
|
||||
description = "Check PEP-8 naming conventions, plugin for flake8"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pep8-naming-0.13.3.tar.gz", hash = "sha256:1705f046dfcd851378aac3be1cd1551c7c1e5ff363bacad707d43007877fa971"},
|
||||
{file = "pep8_naming-0.13.3-py3-none-any.whl", hash = "sha256:1a86b8c71a03337c97181917e2b472f0f5e4ccb06844a0d6f0a33522549e7a80"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
flake8 = ">=5.0.0"
|
||||
dev = ["black", "hypothesis", "mypy", "pygments (>=2.14.0)", "pytest", "pytest-cov", "pytest-timeout", "ruff", "tox", "types-pygments"]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "2.6.2"
|
||||
version = "3.1.1"
|
||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"},
|
||||
{file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"},
|
||||
{file = "platformdirs-3.1.1-py3-none-any.whl", hash = "sha256:e5986afb596e4bb5bde29a79ac9061aa955b94fca2399b7aaac4090860920dd8"},
|
||||
{file = "platformdirs-3.1.1.tar.gz", hash = "sha256:024996549ee88ec1a9aa99ff7f8fc819bb59e2c3477b410d90a16d32d6e707aa"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"]
|
||||
test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
|
||||
docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"]
|
||||
test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
@@ -738,14 +679,14 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pre-commit"
|
||||
version = "3.0.4"
|
||||
version = "3.1.1"
|
||||
description = "A framework for managing and maintaining multi-language pre-commit hooks."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pre_commit-3.0.4-py2.py3-none-any.whl", hash = "sha256:9e3255edb0c9e7fe9b4f328cb3dc86069f8fdc38026f1bf521018a05eaf4d67b"},
|
||||
{file = "pre_commit-3.0.4.tar.gz", hash = "sha256:bc4687478d55578c4ac37272fe96df66f73d9b5cf81be6f28627d4e712e752d5"},
|
||||
{file = "pre_commit-3.1.1-py2.py3-none-any.whl", hash = "sha256:b80254e60668e1dd1f5c03a1c9e0413941d61f568a57d745add265945f65bfe8"},
|
||||
{file = "pre_commit-3.1.1.tar.gz", hash = "sha256:d63e6537f9252d99f65755ae5b79c989b462d511ebbc481b561db6a297e1e865"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -757,14 +698,14 @@ virtualenv = ">=20.10.0"
|
||||
|
||||
[[package]]
|
||||
name = "prompt-toolkit"
|
||||
version = "3.0.36"
|
||||
version = "3.0.38"
|
||||
description = "Library for building powerful interactive command lines in Python"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6.2"
|
||||
python-versions = ">=3.7.0"
|
||||
files = [
|
||||
{file = "prompt_toolkit-3.0.36-py3-none-any.whl", hash = "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305"},
|
||||
{file = "prompt_toolkit-3.0.36.tar.gz", hash = "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63"},
|
||||
{file = "prompt_toolkit-3.0.38-py3-none-any.whl", hash = "sha256:45ea77a2f7c60418850331366c81cf6b5b9cf4c7fd34616f733c5427e6abbb1f"},
|
||||
{file = "prompt_toolkit-3.0.38.tar.gz", hash = "sha256:23ac5d50538a9a38c8bde05fecb47d0b403ecd0662857a86f886f798563d5b9b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -782,30 +723,6 @@ files = [
|
||||
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycodestyle"
|
||||
version = "2.10.0"
|
||||
description = "Python style guide checker"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "pycodestyle-2.10.0-py2.py3-none-any.whl", hash = "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610"},
|
||||
{file = "pycodestyle-2.10.0.tar.gz", hash = "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyflakes"
|
||||
version = "3.0.1"
|
||||
description = "passive checker of Python programs"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "pyflakes-3.0.1-py2.py3-none-any.whl", hash = "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf"},
|
||||
{file = "pyflakes-3.0.1.tar.gz", hash = "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.14.0"
|
||||
@@ -838,14 +755,14 @@ tests = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "7.2.1"
|
||||
version = "7.2.2"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"},
|
||||
{file = "pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"},
|
||||
{file = "pytest-7.2.2-py3-none-any.whl", hash = "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e"},
|
||||
{file = "pytest-7.2.2.tar.gz", hash = "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -914,14 +831,14 @@ test = ["pytest-adaptavist (>=5.1.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-xdist"
|
||||
version = "3.1.0"
|
||||
version = "3.2.0"
|
||||
description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pytest-xdist-3.1.0.tar.gz", hash = "sha256:40fdb8f3544921c5dfcd486ac080ce22870e71d82ced6d2e78fa97c2addd480c"},
|
||||
{file = "pytest_xdist-3.1.0-py3-none-any.whl", hash = "sha256:70a76f191d8a1d2d6be69fc440cdf85f3e4c03c08b520fd5dc5d338d6cf07d89"},
|
||||
{file = "pytest-xdist-3.2.0.tar.gz", hash = "sha256:fa10f95a2564cd91652f2d132725183c3b590d9fdcdec09d3677386ecf4c1ce9"},
|
||||
{file = "pytest_xdist-3.2.0-py3-none-any.whl", hash = "sha256:336098e3bbd8193276867cc87db8b22903c3927665dff9d1ac8684c02f597b68"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -948,6 +865,13 @@ files = [
|
||||
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
|
||||
{file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
|
||||
{file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
|
||||
{file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"},
|
||||
{file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"},
|
||||
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"},
|
||||
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"},
|
||||
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"},
|
||||
{file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"},
|
||||
{file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"},
|
||||
{file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
|
||||
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
|
||||
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
|
||||
@@ -1094,19 +1018,19 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "13.3.1"
|
||||
version = "13.3.2"
|
||||
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7.0"
|
||||
files = [
|
||||
{file = "rich-13.3.1-py3-none-any.whl", hash = "sha256:8aa57747f3fc3e977684f0176a88e789be314a99f99b43b75d1e9cb5dc6db9e9"},
|
||||
{file = "rich-13.3.1.tar.gz", hash = "sha256:125d96d20c92b946b983d0d392b84ff945461e5a06d3867e9f9e575f8697b67f"},
|
||||
{file = "rich-13.3.2-py3-none-any.whl", hash = "sha256:a104f37270bf677148d8acb07d33be1569eeee87e2d1beb286a4e9113caf6f2f"},
|
||||
{file = "rich-13.3.2.tar.gz", hash = "sha256:91954fe80cfb7985727a467ca98a7618e5dd15178cc2da10f553b36a93859001"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
markdown-it-py = ">=2.1.0,<3.0.0"
|
||||
pygments = ">=2.14.0,<3.0.0"
|
||||
markdown-it-py = ">=2.2.0,<3.0.0"
|
||||
pygments = ">=2.13.0,<3.0.0"
|
||||
|
||||
[package.extras]
|
||||
jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
||||
@@ -1146,6 +1070,9 @@ files = [
|
||||
{file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:d000f258cf42fec2b1bbf2863c61d7b8918d31ffee905da62dede869254d3b8a"},
|
||||
{file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:045e0626baf1c52e5527bd5db361bc83180faaba2ff586e763d3d5982a876a9e"},
|
||||
{file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_12_6_arm64.whl", hash = "sha256:721bc4ba4525f53f6a611ec0967bdcee61b31df5a56801281027a3a6d1c2daf5"},
|
||||
{file = "ruamel.yaml.clib-0.2.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:41d0f1fa4c6830176eef5b276af04c89320ea616655d01327d5ce65e50575c94"},
|
||||
{file = "ruamel.yaml.clib-0.2.7-cp311-cp311-win32.whl", hash = "sha256:f6d3d39611ac2e4f62c3128a9eed45f19a6608670c5a2f4f07f24e8de3441d38"},
|
||||
{file = "ruamel.yaml.clib-0.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:da538167284de58a52109a9b89b8f6a53ff8437dd6dc26d33b57bf6699153122"},
|
||||
{file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4b3a93bb9bc662fc1f99c5c3ea8e623d8b23ad22f861eb6fce9377ac07ad6072"},
|
||||
{file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_12_0_arm64.whl", hash = "sha256:a234a20ae07e8469da311e182e70ef6b199d0fbeb6c6cc2901204dd87fb867e8"},
|
||||
{file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:15910ef4f3e537eea7fe45f8a5d19997479940d9196f357152a09031c5be59f3"},
|
||||
@@ -1175,40 +1102,41 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.0.240"
|
||||
version = "0.0.254"
|
||||
description = "An extremely fast Python linter, written in Rust."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.0.240-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:222dd5a5f7cf2f155d7bb77ac484b9afd6f8aaecd963a91c8dbb93355ef42fd2"},
|
||||
{file = "ruff-0.0.240-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:2c956a037671b5ab81546346f3e7f0b3f0e13d0b2e5a3e88c1b2227a1e9aae82"},
|
||||
{file = "ruff-0.0.240-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b43c73fc165f8c7de7c095208d05653744aee6fb0a71680449c2ff1cf59183ea"},
|
||||
{file = "ruff-0.0.240-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f58f1122001150d70909885ccf43d869237be814d4cfc74bb60b3883635e440a"},
|
||||
{file = "ruff-0.0.240-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b427050336b8967755e305f506e84e550591fa47766b5b0cb0c8bcb5c8ca9e7"},
|
||||
{file = "ruff-0.0.240-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0fe8cc47c4c3423548a074e163388f943a14b1e349be88e5dc4cd43df81b6344"},
|
||||
{file = "ruff-0.0.240-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2f40f07d030e7a8cbe365a62fe8543e146b9bcd2a31f5625c2beaccad0d1b8c1"},
|
||||
{file = "ruff-0.0.240-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c222ad12e4bf795e3cec64d56178af1bfbc5d97929a0abf685564937e52c9862"},
|
||||
{file = "ruff-0.0.240-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a26eb3cd68527bcae2543027a0a674d37d03f239f6f025049149115c9775438d"},
|
||||
{file = "ruff-0.0.240-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4591c9104b6898cbd0df57f6b6f8e2907b08fa85ff5196750f0a7b370ae9f78e"},
|
||||
{file = "ruff-0.0.240-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7fed973319ca0a8c2e5c80732217b9b1ec069305839f480907469791e596b150"},
|
||||
{file = "ruff-0.0.240-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4ce049d1fedb1b785fef29403d26e6109b77287b51afd10b74edc986f609c4af"},
|
||||
{file = "ruff-0.0.240-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5127cfaec1f78bd7104174eeacee85dea64796905812b448efd60f504cfa5eec"},
|
||||
{file = "ruff-0.0.240-py3-none-win32.whl", hash = "sha256:071e01a980ffd638a5ce7960ce662fa9b434962f78e7c575478c64e5f147aac8"},
|
||||
{file = "ruff-0.0.240-py3-none-win_amd64.whl", hash = "sha256:d0b1ac5d1d882db25ca4b7dff8aa813ecc7912bdde4ad8f59f2d922b1996cbc7"},
|
||||
{file = "ruff-0.0.240.tar.gz", hash = "sha256:0f1a0b04ce6f3d59894c64f3c3a5a0a35ff4803b8dc51e962d7de42fdb0f5eb1"},
|
||||
{file = "ruff-0.0.254-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:dd58c500d039fb381af8d861ef456c3e94fd6855c3d267d6c6718c9a9fe07be0"},
|
||||
{file = "ruff-0.0.254-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:688379050ae05394a6f9f9c8471587fd5dcf22149bd4304a4ede233cc4ef89a1"},
|
||||
{file = "ruff-0.0.254-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1429be6d8bd3db0bf5becac3a38bd56f8421447790c50599cd90fd53417ec4"},
|
||||
{file = "ruff-0.0.254-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:059a380c08e849b6f312479b18cc63bba2808cff749ad71555f61dd930e3c9a2"},
|
||||
{file = "ruff-0.0.254-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3f15d5d033fd3dcb85d982d6828ddab94134686fac2c02c13a8822aa03e1321"},
|
||||
{file = "ruff-0.0.254-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8deba44fd563361c488dedec90dc330763ee0c01ba54e17df54ef5820079e7e0"},
|
||||
{file = "ruff-0.0.254-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef20bf798ffe634090ad3dc2e8aa6a055f08c448810a2f800ab716cc18b80107"},
|
||||
{file = "ruff-0.0.254-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0deb1d7226ea9da9b18881736d2d96accfa7f328c67b7410478cc064ad1fa6aa"},
|
||||
{file = "ruff-0.0.254-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27d39d697fdd7df1f2a32c1063756ee269ad8d5345c471ee3ca450636d56e8c6"},
|
||||
{file = "ruff-0.0.254-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2fc21d060a3197ac463596a97d9b5db2d429395938b270ded61dd60f0e57eb21"},
|
||||
{file = "ruff-0.0.254-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f70dc93bc9db15cccf2ed2a831938919e3e630993eeea6aba5c84bc274237885"},
|
||||
{file = "ruff-0.0.254-py3-none-musllinux_1_2_i686.whl", hash = "sha256:09c764bc2bd80c974f7ce1f73a46092c286085355a5711126af351b9ae4bea0c"},
|
||||
{file = "ruff-0.0.254-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d4385cdd30153b7aa1d8f75dfd1ae30d49c918ead7de07e69b7eadf0d5538a1f"},
|
||||
{file = "ruff-0.0.254-py3-none-win32.whl", hash = "sha256:c38291bda4c7b40b659e8952167f386e86ec29053ad2f733968ff1d78b4c7e15"},
|
||||
{file = "ruff-0.0.254-py3-none-win_amd64.whl", hash = "sha256:e15742df0f9a3615fbdc1ee9a243467e97e75bf88f86d363eee1ed42cedab1ec"},
|
||||
{file = "ruff-0.0.254-py3-none-win_arm64.whl", hash = "sha256:b435afc4d65591399eaf4b2af86e441a71563a2091c386cadf33eaa11064dc09"},
|
||||
{file = "ruff-0.0.254.tar.gz", hash = "sha256:0eb66c9520151d3bd950ea43b3a088618a8e4e10a5014a72687881e6f3606312"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "67.1.0"
|
||||
version = "67.6.0"
|
||||
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "setuptools-67.1.0-py3-none-any.whl", hash = "sha256:a7687c12b444eaac951ea87a9627c4f904ac757e7abdc5aac32833234af90378"},
|
||||
{file = "setuptools-67.1.0.tar.gz", hash = "sha256:e261cdf010c11a41cb5cb5f1bf3338a7433832029f559a6a7614bd42a967c300"},
|
||||
{file = "setuptools-67.6.0-py3-none-any.whl", hash = "sha256:b78aaa36f6b90a074c1fa651168723acbf45d14cb1196b6f02c0fd07f17623b2"},
|
||||
{file = "setuptools-67.6.0.tar.gz", hash = "sha256:2ee892cd5f29f3373097f5a814697e397cf3ce313616df0af11231e2ad118077"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -1333,48 +1261,48 @@ test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.
|
||||
|
||||
[[package]]
|
||||
name = "types-python-dateutil"
|
||||
version = "2.8.19.6"
|
||||
version = "2.8.19.10"
|
||||
description = "Typing stubs for python-dateutil"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "types-python-dateutil-2.8.19.6.tar.gz", hash = "sha256:4a6f4cc19ce4ba1a08670871e297bf3802f55d4f129e6aa2443f540b6cf803d2"},
|
||||
{file = "types_python_dateutil-2.8.19.6-py3-none-any.whl", hash = "sha256:cfb7d31021c6bce6f3362c69af6e3abb48fe3e08854f02487e844ff910deec2a"},
|
||||
{file = "types-python-dateutil-2.8.19.10.tar.gz", hash = "sha256:c640f2eb71b4b94a9d3bfda4c04250d29a24e51b8bad6e12fddec0cf6e96f7a3"},
|
||||
{file = "types_python_dateutil-2.8.19.10-py3-none-any.whl", hash = "sha256:fbecd02c19cac383bf4a16248d45ffcff17c93a04c0794be5f95d42c6aa5de39"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.4.0"
|
||||
version = "4.5.0"
|
||||
description = "Backported and Experimental Type Hints for Python 3.7+"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
|
||||
{file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
|
||||
{file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"},
|
||||
{file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "20.17.1"
|
||||
version = "20.20.0"
|
||||
description = "Virtual Python Environment builder"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "virtualenv-20.17.1-py3-none-any.whl", hash = "sha256:ce3b1684d6e1a20a3e5ed36795a97dfc6af29bc3970ca8dab93e11ac6094b3c4"},
|
||||
{file = "virtualenv-20.17.1.tar.gz", hash = "sha256:f8b927684efc6f1cc206c9db297a570ab9ad0e51c16fa9e45487d36d1905c058"},
|
||||
{file = "virtualenv-20.20.0-py3-none-any.whl", hash = "sha256:3c22fa5a7c7aa106ced59934d2c20a2ecb7f49b4130b8bf444178a16b880fa45"},
|
||||
{file = "virtualenv-20.20.0.tar.gz", hash = "sha256:a8a4b8ca1e28f864b7514a253f98c1d62b64e31e77325ba279248c65fb4fcef4"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
distlib = ">=0.3.6,<1"
|
||||
filelock = ">=3.4.1,<4"
|
||||
platformdirs = ">=2.4,<3"
|
||||
platformdirs = ">=2.4,<4"
|
||||
|
||||
[package.extras]
|
||||
docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"]
|
||||
testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"]
|
||||
docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"]
|
||||
test = ["covdefaults (>=2.2.2)", "coverage (>=7.1)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23)", "pytest (>=7.2.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "vulture"
|
||||
@@ -1421,4 +1349,4 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "5bb4866827da1d2e417218c8120be075f1a61a25f015d97183feb63098c64afa"
|
||||
content-hash = "b77f1653a71eca187c8d47f8b27d2677191fe37d932fc9ae8e696c462d2ea999"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[virtualenvs]
|
||||
in-project = true
|
||||
in-project = true
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
name = "obsidian-metadata"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/natelandau/obsidian-metadata"
|
||||
version = "0.5.0"
|
||||
version = "0.7.0"
|
||||
|
||||
[tool.poetry.scripts] # https://python-poetry.org/docs/pyproject/#scripts
|
||||
obsidian-metadata = "obsidian_metadata.cli:app"
|
||||
@@ -21,34 +21,32 @@
|
||||
python = "^3.10"
|
||||
questionary = "^1.10.0"
|
||||
regex = "^2022.10.31"
|
||||
rich = "^13.2.0"
|
||||
rich = "^13.3.2"
|
||||
ruamel-yaml = "^0.17.21"
|
||||
shellingham = "^1.4.0"
|
||||
shellingham = "^1.5.0.post1"
|
||||
tomlkit = "^0.11.6"
|
||||
typer = "^0.7.0"
|
||||
|
||||
[tool.poetry.group.test.dependencies]
|
||||
pytest = "^7.2.0"
|
||||
pytest = "^7.2.2"
|
||||
pytest-clarity = "^1.0.1"
|
||||
pytest-mock = "^3.10.0"
|
||||
pytest-pretty-terminal = "^1.1.0"
|
||||
pytest-xdist = "^3.1.0"
|
||||
pytest-xdist = "^3.2.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
absolufy-imports = "^0.3.1"
|
||||
black = "^23.1.0"
|
||||
commitizen = "^2.40.0"
|
||||
coverage = "^7.1.0"
|
||||
commitizen = "^2.42.1"
|
||||
coverage = "^7.2.1"
|
||||
interrogate = "^1.5.0"
|
||||
mypy = "^0.991"
|
||||
pdoc = "^12.3.1"
|
||||
pep8-naming = "^0.13.3"
|
||||
poethepoet = "^0.18.0"
|
||||
pre-commit = "^3.0.4"
|
||||
mypy = "^1.1.1"
|
||||
pdoc = "^13.0.0"
|
||||
poethepoet = "^0.18.1"
|
||||
pre-commit = "^3.1.1"
|
||||
pysnooper = "^1.1.1"
|
||||
ruff = "^0.0.240"
|
||||
ruff = "^0.0.254"
|
||||
typeguard = "^2.13.3"
|
||||
types-python-dateutil = "^2.8.19.5"
|
||||
types-python-dateutil = "^2.8.19.10"
|
||||
vulture = "^2.7"
|
||||
|
||||
[tool.ruff] # https://github.com/charliermarsh/ruff
|
||||
@@ -75,12 +73,7 @@
|
||||
]
|
||||
ignore-init-module-imports = true
|
||||
line-length = 100
|
||||
per-file-ignores = { "cli.py" = [
|
||||
"PLR0913",
|
||||
], "tests/*.py" = [
|
||||
"E999",
|
||||
"PLR2004",
|
||||
] }
|
||||
per-file-ignores = { "cli.py" = ["PLR0912", "PLR0913"], "tests/*.py" = ["PLR0913", "PLR2004"] }
|
||||
select = [
|
||||
"A",
|
||||
"B",
|
||||
@@ -145,15 +138,12 @@
|
||||
line-length = 100
|
||||
|
||||
[tool.commitizen]
|
||||
bump_message = "bump(release): v$current_version → v$new_version"
|
||||
changelog_incremental = true
|
||||
tag_format = "v$version"
|
||||
bump_message = "bump(release): v$current_version → v$new_version"
|
||||
changelog_incremental = true
|
||||
tag_format = "v$version"
|
||||
update_changelog_on_bump = true
|
||||
version = "0.5.0"
|
||||
version_files = [
|
||||
"pyproject.toml:version",
|
||||
"src/obsidian_metadata/__version__.py:__version__",
|
||||
]
|
||||
version = "0.7.0"
|
||||
version_files = ["pyproject.toml:version", "src/obsidian_metadata/__version__.py:__version__"]
|
||||
|
||||
[tool.interrogate]
|
||||
exclude = ["build", "docs", "tests"]
|
||||
@@ -213,7 +203,7 @@
|
||||
help = "Lint this package"
|
||||
|
||||
[[tool.poe.tasks.lint.sequence]]
|
||||
shell = "ruff --extend-ignore=I001,D301,D401,PLR2004,PLR0913 src/"
|
||||
shell = "ruff --extend-ignore=I001,D301,D401 src/"
|
||||
|
||||
[[tool.poe.tasks.lint.sequence]]
|
||||
shell = "black --check src/ tests/"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
"""obsidian-metadata version."""
|
||||
__version__ = "0.5.0"
|
||||
__version__ = "0.7.0"
|
||||
|
||||
@@ -122,9 +122,9 @@ class Config:
|
||||
# Folders within the vault to ignore when indexing metadata
|
||||
exclude_paths = [".git", ".obsidian"]
|
||||
|
||||
# Location to add metadata. One of:
|
||||
# Location to add new metadata. One of:
|
||||
# TOP: Directly after frontmatter.
|
||||
# AFTER_TITLE: After a header following frontmatter.
|
||||
# AFTER_TITLE: After the first header following frontmatter.
|
||||
# BOTTOM: The bottom of the note
|
||||
insert_location = "BOTTOM"
|
||||
"""
|
||||
@@ -164,6 +164,7 @@ class VaultConfig:
|
||||
yield "config", self.config
|
||||
yield "path", self.path
|
||||
yield "exclude_paths", self.exclude_paths
|
||||
yield "insert_location", self.insert_location
|
||||
|
||||
def _validate_vault_path(self, vault_path: Path | None) -> Path:
|
||||
"""Validate the vault path."""
|
||||
|
||||
@@ -7,7 +7,8 @@ from textwrap import wrap
|
||||
import rich.repr
|
||||
import typer
|
||||
from loguru import logger
|
||||
from rich import print
|
||||
|
||||
from obsidian_metadata._utils.console import console
|
||||
|
||||
|
||||
class LogLevel(Enum):
|
||||
@@ -38,7 +39,7 @@ def dryrun(msg: str) -> None:
|
||||
Args:
|
||||
msg: Message to print
|
||||
"""
|
||||
print(f"[cyan]DRYRUN | {msg}[/cyan]")
|
||||
console.print(f"[cyan]DRYRUN | {msg}[/cyan]")
|
||||
|
||||
|
||||
def success(msg: str) -> None:
|
||||
@@ -47,7 +48,7 @@ def success(msg: str) -> None:
|
||||
Args:
|
||||
msg: Message to print
|
||||
"""
|
||||
print(f"[green]SUCCESS | {msg}[/green]")
|
||||
console.print(f"[green]SUCCESS | {msg}[/green]")
|
||||
|
||||
|
||||
def warning(msg: str) -> None:
|
||||
@@ -56,7 +57,7 @@ def warning(msg: str) -> None:
|
||||
Args:
|
||||
msg: Message to print
|
||||
"""
|
||||
print(f"[yellow]WARNING | {msg}[/yellow]")
|
||||
console.print(f"[yellow]WARNING | {msg}[/yellow]")
|
||||
|
||||
|
||||
def error(msg: str) -> None:
|
||||
@@ -65,7 +66,7 @@ def error(msg: str) -> None:
|
||||
Args:
|
||||
msg: Message to print
|
||||
"""
|
||||
print(f"[red]ERROR | {msg}[/red]")
|
||||
console.print(f"[red]ERROR | {msg}[/red]")
|
||||
|
||||
|
||||
def notice(msg: str) -> None:
|
||||
@@ -74,7 +75,7 @@ def notice(msg: str) -> None:
|
||||
Args:
|
||||
msg: Message to print
|
||||
"""
|
||||
print(f"[bold]NOTICE | {msg}[/bold]")
|
||||
console.print(f"[bold]NOTICE | {msg}[/bold]")
|
||||
|
||||
|
||||
def info(msg: str) -> None:
|
||||
@@ -83,7 +84,7 @@ def info(msg: str) -> None:
|
||||
Args:
|
||||
msg: Message to print
|
||||
"""
|
||||
print(f"INFO | {msg}")
|
||||
console.print(f"INFO | {msg}")
|
||||
|
||||
|
||||
def usage(msg: str, width: int = 80) -> None:
|
||||
@@ -95,9 +96,9 @@ def usage(msg: str, width: int = 80) -> None:
|
||||
"""
|
||||
for _n, line in enumerate(wrap(msg, width=width)):
|
||||
if _n == 0:
|
||||
print(f"[dim]USAGE | {line}")
|
||||
console.print(f"[dim]USAGE | {line}")
|
||||
else:
|
||||
print(f"[dim] | {line}")
|
||||
console.print(f"[dim] | {line}")
|
||||
|
||||
|
||||
def debug(msg: str) -> None:
|
||||
@@ -106,7 +107,7 @@ def debug(msg: str) -> None:
|
||||
Args:
|
||||
msg: Message to print
|
||||
"""
|
||||
print(f"[blue]DEBUG | {msg}[/blue]")
|
||||
console.print(f"[blue]DEBUG | {msg}[/blue]")
|
||||
|
||||
|
||||
def dim(msg: str) -> None:
|
||||
@@ -115,7 +116,7 @@ def dim(msg: str) -> None:
|
||||
Args:
|
||||
msg: Message to print
|
||||
"""
|
||||
print(f"[dim]{msg}[/dim]")
|
||||
console.print(f"[dim]{msg}[/dim]")
|
||||
|
||||
|
||||
def _log_formatter(record: dict) -> str:
|
||||
@@ -171,7 +172,7 @@ class LoggerManager:
|
||||
self.log_level = log_level
|
||||
|
||||
if self.log_file == Path("/logs") and self.log_to_file: # pragma: no cover
|
||||
print("No log file specified")
|
||||
console.print("No log file specified")
|
||||
raise typer.Exit(1)
|
||||
|
||||
if self.verbosity >= VerboseLevel.TRACE.value:
|
||||
@@ -239,7 +240,7 @@ class LoggerManager:
|
||||
"""
|
||||
if self.log_level <= LogLevel.TRACE.value:
|
||||
if msg:
|
||||
print(msg)
|
||||
console.print(msg)
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -254,7 +255,7 @@ class LoggerManager:
|
||||
"""
|
||||
if self.log_level <= LogLevel.DEBUG.value:
|
||||
if msg:
|
||||
print(msg)
|
||||
console.print(msg)
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -269,7 +270,7 @@ class LoggerManager:
|
||||
"""
|
||||
if self.log_level <= LogLevel.INFO.value:
|
||||
if msg:
|
||||
print(msg)
|
||||
console.print(msg)
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -284,6 +285,6 @@ class LoggerManager:
|
||||
"""
|
||||
if self.log_level <= LogLevel.WARNING.value:
|
||||
if msg:
|
||||
print(msg)
|
||||
console.print(msg)
|
||||
return True
|
||||
return False # pragma: no cover
|
||||
|
||||
4
src/obsidian_metadata/_utils/console.py
Normal file
4
src/obsidian_metadata/_utils/console.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""Rich console object for the application."""
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
@@ -6,6 +6,7 @@ from typing import Any
|
||||
import typer
|
||||
|
||||
from obsidian_metadata.__version__ import __version__
|
||||
from obsidian_metadata._utils.console import console
|
||||
|
||||
|
||||
def clean_dictionary(dictionary: dict[str, Any]) -> dict[str, Any]:
|
||||
@@ -34,7 +35,7 @@ def clear_screen() -> None: # pragma: no cover
|
||||
def dict_contains(
|
||||
dictionary: dict[str, list[str]], key: str, value: str = None, is_regex: bool = False
|
||||
) -> bool:
|
||||
"""Check if a dictionary contains a key.
|
||||
"""Check if a dictionary contains a key or if a specified key contains a value.
|
||||
|
||||
Args:
|
||||
dictionary (dict): Dictionary to check
|
||||
@@ -62,12 +63,15 @@ def dict_contains(
|
||||
return key in dictionary and value in dictionary[key]
|
||||
|
||||
|
||||
def dict_values_to_lists_strings(dictionary: dict, strip_null_values: bool = False) -> dict:
|
||||
def dict_values_to_lists_strings(
|
||||
dictionary: dict,
|
||||
strip_null_values: bool = False,
|
||||
) -> dict:
|
||||
"""Convert all values in a dictionary to lists of strings.
|
||||
|
||||
Args:
|
||||
dictionary (dict): Dictionary to convert
|
||||
strip_null (bool): Whether to strip null values
|
||||
strip_null_values (bool): Whether to strip null values
|
||||
|
||||
Returns:
|
||||
dict: Dictionary with all values converted to lists of strings
|
||||
@@ -122,7 +126,7 @@ def docstring_parameter(*sub: Any) -> Any:
|
||||
|
||||
|
||||
def merge_dictionaries(dict1: dict, dict2: dict) -> dict:
|
||||
"""Merge two dictionaries.
|
||||
"""Merge two dictionaries. When the values are lists, they are merged and sorted.
|
||||
|
||||
Args:
|
||||
dict1 (dict): First dictionary.
|
||||
@@ -181,5 +185,5 @@ def remove_markdown_sections(
|
||||
def version_callback(value: bool) -> None:
|
||||
"""Print version and exit."""
|
||||
if value:
|
||||
print(f"{__package__.split('.')[0]}: v{__version__}")
|
||||
console.print(f"{__package__.split('.')[0]}: v{__version__}")
|
||||
raise typer.Exit()
|
||||
|
||||
@@ -5,7 +5,6 @@ from typing import Optional
|
||||
|
||||
import questionary
|
||||
import typer
|
||||
from rich import print
|
||||
|
||||
from obsidian_metadata._config import Config
|
||||
from obsidian_metadata._utils import (
|
||||
@@ -14,6 +13,7 @@ from obsidian_metadata._utils import (
|
||||
docstring_parameter,
|
||||
version_callback,
|
||||
)
|
||||
from obsidian_metadata._utils.console import console
|
||||
from obsidian_metadata.models import Application
|
||||
|
||||
app = typer.Typer(add_completion=False, no_args_is_help=True, rich_markup_mode="rich")
|
||||
@@ -135,6 +135,14 @@ def main(
|
||||
• Delete a value from a key
|
||||
• Delete an inline tag
|
||||
|
||||
[bold underline]Transpose Metadata[/]
|
||||
Move metadata from inline to frontmatter or the reverse.
|
||||
|
||||
• Transpose all metadata - Moves all frontmatter to inline
|
||||
metadata, or the reverse
|
||||
• Transpose key - Transposes a specific key and all it's values
|
||||
• Transpose value- Transpose a specific key:value pair
|
||||
|
||||
[bold underline]Review Changes[/]
|
||||
Prior to committing changes, review all changes that will be made.
|
||||
• View a diff of the changes that will be made
|
||||
@@ -163,7 +171,7 @@ def main(
|
||||
|_| |_|\___|\__\__,_|\__,_|\__,_|\__\__,_|
|
||||
"""
|
||||
clear_screen()
|
||||
print(banner)
|
||||
console.print(banner)
|
||||
|
||||
config: Config = Config(config_path=config_file, vault_path=vault_path)
|
||||
if len(config.vaults) == 0:
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
"""Questions for the cli."""
|
||||
|
||||
|
||||
from typing import Any
|
||||
from pathlib import Path
|
||||
import questionary
|
||||
from rich import print
|
||||
from rich import box
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from obsidian_metadata._config import VaultConfig
|
||||
from obsidian_metadata._utils.alerts import logger as log
|
||||
from obsidian_metadata.models import Patterns, Vault, VaultFilter
|
||||
from obsidian_metadata._utils import alerts
|
||||
from obsidian_metadata.models.questions import Questions
|
||||
from obsidian_metadata.models.enums import MetadataType
|
||||
from typing import Any
|
||||
|
||||
PATTERNS = Patterns()
|
||||
import questionary
|
||||
import typer
|
||||
from rich import box
|
||||
from rich.table import Table
|
||||
|
||||
from obsidian_metadata._config import VaultConfig
|
||||
from obsidian_metadata._utils import alerts
|
||||
from obsidian_metadata._utils.console import console
|
||||
from obsidian_metadata.models import Vault, VaultFilter
|
||||
from obsidian_metadata.models.enums import MetadataType
|
||||
from obsidian_metadata.models.questions import Questions
|
||||
|
||||
|
||||
class Application:
|
||||
@@ -34,7 +33,6 @@ class Application:
|
||||
|
||||
def _load_vault(self) -> None:
|
||||
"""Load the vault."""
|
||||
|
||||
if len(self.filters) == 0:
|
||||
self.vault: Vault = Vault(config=self.config, dry_run=self.dry_run)
|
||||
else:
|
||||
@@ -52,7 +50,7 @@ class Application:
|
||||
while True:
|
||||
self.vault.info()
|
||||
|
||||
match self.questions.ask_application_main(): # noqa: E999
|
||||
match self.questions.ask_application_main():
|
||||
case "vault_actions":
|
||||
self.application_vault()
|
||||
case "inspect_metadata":
|
||||
@@ -65,6 +63,8 @@ class Application:
|
||||
self.application_rename_metadata()
|
||||
case "delete_metadata":
|
||||
self.application_delete_metadata()
|
||||
case "transpose_metadata":
|
||||
self.application_transpose_metadata()
|
||||
case "review_changes":
|
||||
self.review_changes()
|
||||
case "commit_changes":
|
||||
@@ -72,7 +72,7 @@ class Application:
|
||||
case _:
|
||||
break
|
||||
|
||||
print("Done!")
|
||||
console.print("Done!")
|
||||
return
|
||||
|
||||
def application_add_metadata(self) -> None:
|
||||
@@ -98,7 +98,7 @@ class Application:
|
||||
area=area, key=key, value=value, location=self.vault.insert_location
|
||||
)
|
||||
if num_changed == 0: # pragma: no cover
|
||||
alerts.warning(f"No notes were changed")
|
||||
alerts.warning("No notes were changed")
|
||||
return
|
||||
|
||||
alerts.success(f"Added metadata to {num_changed} notes")
|
||||
@@ -113,14 +113,60 @@ class Application:
|
||||
)
|
||||
|
||||
if num_changed == 0: # pragma: no cover
|
||||
alerts.warning(f"No notes were changed")
|
||||
alerts.warning("No notes were changed")
|
||||
return
|
||||
|
||||
alerts.success(f"Added metadata to {num_changed} notes")
|
||||
case _: # pragma: no cover
|
||||
return
|
||||
|
||||
def application_filter(self) -> None:
|
||||
def application_delete_metadata(self) -> None:
|
||||
"""Delete metadata."""
|
||||
alerts.usage("Delete either a key and all associated values, or a specific value.")
|
||||
|
||||
choices = [
|
||||
{"name": "Delete inline tag", "value": "delete_inline_tag"},
|
||||
{"name": "Delete key", "value": "delete_key"},
|
||||
{"name": "Delete value", "value": "delete_value"},
|
||||
questionary.Separator(),
|
||||
{"name": "Back", "value": "back"},
|
||||
]
|
||||
match self.questions.ask_selection(
|
||||
choices=choices, question="Select a metadata type to delete"
|
||||
):
|
||||
case "delete_key":
|
||||
self.delete_key()
|
||||
case "delete_value":
|
||||
self.delete_value()
|
||||
case "delete_inline_tag":
|
||||
self.delete_inline_tag()
|
||||
case _: # pragma: no cover
|
||||
return
|
||||
|
||||
def application_rename_metadata(self) -> None:
|
||||
"""Rename metadata."""
|
||||
alerts.usage("Select the type of metadata to rename.")
|
||||
|
||||
choices = [
|
||||
{"name": "Rename inline tag", "value": "rename_inline_tag"},
|
||||
{"name": "Rename key", "value": "rename_key"},
|
||||
{"name": "Rename value", "value": "rename_value"},
|
||||
questionary.Separator(),
|
||||
{"name": "Back", "value": "back"},
|
||||
]
|
||||
match self.questions.ask_selection(
|
||||
choices=choices, question="Select a metadata type to rename"
|
||||
):
|
||||
case "rename_key":
|
||||
self.rename_key()
|
||||
case "rename_value":
|
||||
self.rename_value()
|
||||
case "rename_inline_tag":
|
||||
self.rename_inline_tag()
|
||||
case _: # pragma: no cover
|
||||
return
|
||||
|
||||
def application_filter(self) -> None: # noqa: C901,PLR0911,PLR0912
|
||||
"""Filter notes."""
|
||||
alerts.usage("Limit the scope of notes to be processed with one or more filters.")
|
||||
|
||||
@@ -173,7 +219,7 @@ class Application:
|
||||
alerts.notice("No filters have been applied")
|
||||
return
|
||||
|
||||
print("")
|
||||
console.print("")
|
||||
table = Table(
|
||||
"Opt",
|
||||
"Filter",
|
||||
@@ -182,34 +228,34 @@ class Application:
|
||||
show_header=False,
|
||||
box=box.HORIZONTALS,
|
||||
)
|
||||
for _n, filter in enumerate(self.filters, start=1):
|
||||
if filter.path_filter is not None:
|
||||
for _n, _filter in enumerate(self.filters, start=1):
|
||||
if _filter.path_filter is not None:
|
||||
table.add_row(
|
||||
str(_n),
|
||||
f"Path regex: [tan bold]{filter.path_filter}",
|
||||
f"Path regex: [tan bold]{_filter.path_filter}",
|
||||
end_section=bool(_n == len(self.filters)),
|
||||
)
|
||||
elif filter.tag_filter is not None:
|
||||
elif _filter.tag_filter is not None:
|
||||
table.add_row(
|
||||
str(_n),
|
||||
f"Tag filter: [tan bold]{filter.tag_filter}",
|
||||
f"Tag filter: [tan bold]{_filter.tag_filter}",
|
||||
end_section=bool(_n == len(self.filters)),
|
||||
)
|
||||
elif filter.key_filter is not None and filter.value_filter is None:
|
||||
elif _filter.key_filter is not None and _filter.value_filter is None:
|
||||
table.add_row(
|
||||
str(_n),
|
||||
f"Key filter: [tan bold]{filter.key_filter}",
|
||||
f"Key filter: [tan bold]{_filter.key_filter}",
|
||||
end_section=bool(_n == len(self.filters)),
|
||||
)
|
||||
elif filter.key_filter is not None and filter.value_filter is not None:
|
||||
elif _filter.key_filter is not None and _filter.value_filter is not None:
|
||||
table.add_row(
|
||||
str(_n),
|
||||
f"Key/Value : [tan bold]{filter.key_filter}={filter.value_filter}",
|
||||
f"Key/Value : [tan bold]{_filter.key_filter}={_filter.value_filter}",
|
||||
end_section=bool(_n == len(self.filters)),
|
||||
)
|
||||
table.add_row(f"{len(self.filters) + 1}", "Clear All")
|
||||
table.add_row(f"{len(self.filters) + 2}", "Return to Main Menu")
|
||||
Console().print(table)
|
||||
console.print(table)
|
||||
|
||||
num = self.questions.ask_number(
|
||||
question="Enter the number of the filter to clear"
|
||||
@@ -238,11 +284,11 @@ class Application:
|
||||
)
|
||||
|
||||
choices = [
|
||||
{"name": "View all metadata", "value": "all_metadata"},
|
||||
{"name": "View all frontmatter", "value": "all_frontmatter"},
|
||||
{"name": "View all inline_metadata", "value": "all_inline"},
|
||||
{"name": "View all keys", "value": "all_keys"},
|
||||
{"name": "View all inline metadata", "value": "all_inline"},
|
||||
{"name": "View all inline tags", "value": "all_tags"},
|
||||
{"name": "View all keys", "value": "all_keys"},
|
||||
{"name": "View all metadata", "value": "all_metadata"},
|
||||
questionary.Separator(),
|
||||
{"name": "Write all metadata to CSV", "value": "export_csv"},
|
||||
{"name": "Write all metadata to JSON file", "value": "export_json"},
|
||||
@@ -252,40 +298,58 @@ class Application:
|
||||
while True:
|
||||
match self.questions.ask_selection(choices=choices, question="Select a vault action"):
|
||||
case "all_metadata":
|
||||
print("")
|
||||
console.print("")
|
||||
self.vault.metadata.print_metadata(area=MetadataType.ALL)
|
||||
print("")
|
||||
console.print("")
|
||||
case "all_frontmatter":
|
||||
print("")
|
||||
console.print("")
|
||||
self.vault.metadata.print_metadata(area=MetadataType.FRONTMATTER)
|
||||
print("")
|
||||
console.print("")
|
||||
case "all_inline":
|
||||
print("")
|
||||
console.print("")
|
||||
self.vault.metadata.print_metadata(area=MetadataType.INLINE)
|
||||
print("")
|
||||
console.print("")
|
||||
case "all_keys":
|
||||
print("")
|
||||
console.print("")
|
||||
self.vault.metadata.print_metadata(area=MetadataType.KEYS)
|
||||
print("")
|
||||
console.print("")
|
||||
case "all_tags":
|
||||
print("")
|
||||
console.print("")
|
||||
self.vault.metadata.print_metadata(area=MetadataType.TAGS)
|
||||
print("")
|
||||
console.print("")
|
||||
case "export_csv":
|
||||
path = self.questions.ask_path(question="Enter a path for the CSV file")
|
||||
if path is None:
|
||||
return
|
||||
self.vault.export_metadata(path=path, format="csv")
|
||||
self.vault.export_metadata(path=path, export_format="csv")
|
||||
alerts.success(f"Metadata written to {path}")
|
||||
case "export_json":
|
||||
path = self.questions.ask_path(question="Enter a path for the JSON file")
|
||||
if path is None:
|
||||
return
|
||||
self.vault.export_metadata(path=path, format="json")
|
||||
self.vault.export_metadata(path=path, export_format="json")
|
||||
alerts.success(f"Metadata written to {path}")
|
||||
case _:
|
||||
return
|
||||
|
||||
def application_transpose_metadata(self) -> None:
|
||||
"""Transpose metadata."""
|
||||
alerts.usage("Transpose metadata from frontmatter to inline or vice versa.")
|
||||
|
||||
choices = [
|
||||
{"name": "Transpose frontmatter to inline", "value": "frontmatter_to_inline"},
|
||||
{"name": "Transpose inline to frontmatter", "value": "inline_to_frontmatter"},
|
||||
]
|
||||
match self.questions.ask_selection(
|
||||
choices=choices, question="Select metadata to transpose"
|
||||
):
|
||||
case "frontmatter_to_inline":
|
||||
self.transpose_metadata(begin=MetadataType.FRONTMATTER, end=MetadataType.INLINE)
|
||||
case "inline_to_frontmatter":
|
||||
self.transpose_metadata(begin=MetadataType.INLINE, end=MetadataType.FRONTMATTER)
|
||||
case _: # pragma: no cover
|
||||
return
|
||||
|
||||
def application_vault(self) -> None:
|
||||
"""Vault actions."""
|
||||
alerts.usage("Create or delete a backup of your vault.")
|
||||
@@ -306,51 +370,6 @@ class Application:
|
||||
case _:
|
||||
return
|
||||
|
||||
def application_delete_metadata(self) -> None:
|
||||
alerts.usage("Delete either a key and all associated values, or a specific value.")
|
||||
|
||||
choices = [
|
||||
{"name": "Delete key", "value": "delete_key"},
|
||||
{"name": "Delete value", "value": "delete_value"},
|
||||
{"name": "Delete inline tag", "value": "delete_inline_tag"},
|
||||
questionary.Separator(),
|
||||
{"name": "Back", "value": "back"},
|
||||
]
|
||||
match self.questions.ask_selection(
|
||||
choices=choices, question="Select a metadata type to delete"
|
||||
):
|
||||
case "delete_key":
|
||||
self.delete_key()
|
||||
case "delete_value":
|
||||
self.delete_value()
|
||||
case "delete_inline_tag":
|
||||
self.delete_inline_tag()
|
||||
case _: # pragma: no cover
|
||||
return
|
||||
|
||||
def application_rename_metadata(self) -> None:
|
||||
"""Rename metadata."""
|
||||
alerts.usage("Select the type of metadata to rename.")
|
||||
|
||||
choices = [
|
||||
{"name": "Rename key", "value": "rename_key"},
|
||||
{"name": "Rename value", "value": "rename_value"},
|
||||
{"name": "Rename inline tag", "value": "rename_inline_tag"},
|
||||
questionary.Separator(),
|
||||
{"name": "Back", "value": "back"},
|
||||
]
|
||||
match self.questions.ask_selection(
|
||||
choices=choices, question="Select a metadata type to rename"
|
||||
):
|
||||
case "rename_key":
|
||||
self.rename_key()
|
||||
case "rename_value":
|
||||
self.rename_value()
|
||||
case "rename_inline_tag":
|
||||
self.rename_inline_tag()
|
||||
case _: # pragma: no cover
|
||||
return
|
||||
|
||||
def commit_changes(self) -> bool:
|
||||
"""Write all changes to disk.
|
||||
|
||||
@@ -360,7 +379,7 @@ class Application:
|
||||
changed_notes = self.vault.get_changed_notes()
|
||||
|
||||
if len(changed_notes) == 0:
|
||||
print("\n")
|
||||
console.print("\n")
|
||||
alerts.notice("No changes to commit.\n")
|
||||
return False
|
||||
|
||||
@@ -375,7 +394,7 @@ class Application:
|
||||
|
||||
if not self.dry_run:
|
||||
alerts.success(f"{len(changed_notes)} changes committed to disk. Exiting")
|
||||
return True
|
||||
raise typer.Exit(0)
|
||||
|
||||
return True
|
||||
|
||||
@@ -385,7 +404,7 @@ class Application:
|
||||
|
||||
num_changed = self.vault.delete_inline_tag(tag)
|
||||
if num_changed == 0:
|
||||
alerts.warning(f"No notes were changed")
|
||||
alerts.warning("No notes were changed")
|
||||
return
|
||||
|
||||
alerts.success(f"Deleted inline tag: {tag} in {num_changed} notes")
|
||||
@@ -435,18 +454,17 @@ class Application:
|
||||
def noninteractive_export_csv(self, path: Path) -> None:
|
||||
"""Export the vault metadata to CSV."""
|
||||
self._load_vault()
|
||||
self.vault.export_metadata(format="json", path=str(path))
|
||||
self.vault.export_metadata(export_format="json", path=str(path))
|
||||
alerts.success(f"Exported metadata to {path}")
|
||||
|
||||
def noninteractive_export_json(self, path: Path) -> None:
|
||||
"""Export the vault metadata to JSON."""
|
||||
self._load_vault()
|
||||
self.vault.export_metadata(format="json", path=str(path))
|
||||
self.vault.export_metadata(export_format="json", path=str(path))
|
||||
alerts.success(f"Exported metadata to {path}")
|
||||
|
||||
def rename_key(self) -> None:
|
||||
"""Renames a key in the vault."""
|
||||
|
||||
"""Rename a key in the vault."""
|
||||
original_key = self.questions.ask_existing_key(
|
||||
question="Which key would you like to rename?"
|
||||
)
|
||||
@@ -459,7 +477,7 @@ class Application:
|
||||
|
||||
num_changed = self.vault.rename_metadata(original_key, new_key)
|
||||
if num_changed == 0:
|
||||
alerts.warning(f"No notes were changed")
|
||||
alerts.warning("No notes were changed")
|
||||
return
|
||||
|
||||
alerts.success(
|
||||
@@ -468,7 +486,6 @@ class Application:
|
||||
|
||||
def rename_inline_tag(self) -> None:
|
||||
"""Rename an inline tag."""
|
||||
|
||||
original_tag = self.questions.ask_existing_inline_tag(question="Which tag to rename?")
|
||||
if original_tag is None: # pragma: no cover
|
||||
return
|
||||
@@ -479,7 +496,7 @@ class Application:
|
||||
|
||||
num_changed = self.vault.rename_inline_tag(original_tag, new_tag)
|
||||
if num_changed == 0:
|
||||
alerts.warning(f"No notes were changed")
|
||||
alerts.warning("No notes were changed")
|
||||
return
|
||||
|
||||
alerts.success(
|
||||
@@ -504,7 +521,7 @@ class Application:
|
||||
|
||||
num_changes = self.vault.rename_metadata(key, value, new_value)
|
||||
if num_changes == 0:
|
||||
alerts.warning(f"No notes were changed")
|
||||
alerts.warning("No notes were changed")
|
||||
return
|
||||
|
||||
alerts.success(f"Renamed '{key}:{value}' to '{key}:{new_value}' in {num_changes} notes")
|
||||
@@ -517,14 +534,8 @@ class Application:
|
||||
alerts.info("No changes to review.")
|
||||
return
|
||||
|
||||
print(f"\nFound {len(changed_notes)} changed notes in the vault.\n")
|
||||
answer = self.questions.ask_confirm(
|
||||
question="View diffs of individual files?", default=False
|
||||
)
|
||||
if not answer: # pragma: no cover
|
||||
return
|
||||
|
||||
choices: list[dict[str, Any] | questionary.Separator] = [questionary.Separator()]
|
||||
alerts.info(f"Found {len(changed_notes)} changed notes in the vault")
|
||||
choices: list[dict[str, Any] | questionary.Separator] = []
|
||||
for n, note in enumerate(changed_notes, start=1):
|
||||
_selection = {
|
||||
"name": f"{n}: {note.note_path.relative_to(self.vault.vault_path)}",
|
||||
@@ -538,8 +549,81 @@ class Application:
|
||||
while True:
|
||||
note_to_review = self.questions.ask_selection(
|
||||
choices=choices,
|
||||
question="Select a new to view the diff",
|
||||
question="Select an updated note to view the diff",
|
||||
)
|
||||
if note_to_review is None or note_to_review == "return":
|
||||
break
|
||||
changed_notes[note_to_review].print_diff()
|
||||
|
||||
def transpose_metadata(self, begin: MetadataType, end: MetadataType) -> None: # noqa: PLR0911
|
||||
"""Transpose metadata from one format to another.
|
||||
|
||||
Args:
|
||||
begin: The format to transpose from.
|
||||
end: The format to transpose to.
|
||||
"""
|
||||
choices = [
|
||||
{"name": f"Transpose all {begin.value} to {end.value}", "value": "transpose_all"},
|
||||
{"name": "Transpose a key", "value": "transpose_key"},
|
||||
{"name": "Transpose a value", "value": "transpose_value"},
|
||||
{"name": "Back", "value": "back"},
|
||||
]
|
||||
match self.questions.ask_selection(choices=choices, question="Select an action to perform"):
|
||||
case "transpose_all":
|
||||
num_changed = self.vault.transpose_metadata(
|
||||
begin=begin,
|
||||
end=end,
|
||||
location=self.vault.insert_location,
|
||||
)
|
||||
|
||||
if num_changed == 0:
|
||||
alerts.warning("No notes were changed")
|
||||
return
|
||||
|
||||
alerts.success(f"Transposed {begin.value} to {end.value} in {num_changed} notes")
|
||||
case "transpose_key":
|
||||
key = self.questions.ask_existing_key(question="Which key to transpose?")
|
||||
if key is None: # pragma: no cover
|
||||
return
|
||||
|
||||
num_changed = self.vault.transpose_metadata(
|
||||
begin=begin,
|
||||
end=end,
|
||||
key=key,
|
||||
location=self.vault.insert_location,
|
||||
)
|
||||
|
||||
if num_changed == 0:
|
||||
alerts.warning("No notes were changed")
|
||||
return
|
||||
|
||||
alerts.success(
|
||||
f"Transposed key: `{key}` from {begin.value} to {end.value} in {num_changed} notes"
|
||||
)
|
||||
case "transpose_value":
|
||||
key = self.questions.ask_existing_key(question="Which key contains the value?")
|
||||
if key is None: # pragma: no cover
|
||||
return
|
||||
|
||||
questions2 = Questions(vault=self.vault, key=key)
|
||||
value = questions2.ask_existing_value(question="Which value to transpose?")
|
||||
if value is None: # pragma: no cover
|
||||
return
|
||||
|
||||
num_changed = self.vault.transpose_metadata(
|
||||
begin=begin,
|
||||
end=end,
|
||||
key=key,
|
||||
value=value,
|
||||
location=self.vault.insert_location,
|
||||
)
|
||||
|
||||
if num_changed == 0:
|
||||
alerts.warning("No notes were changed")
|
||||
return
|
||||
|
||||
alerts.success(
|
||||
f"Transposed key: `{key}:{value}` from {begin.value} to {end.value} in {num_changed} notes"
|
||||
)
|
||||
case _:
|
||||
return
|
||||
|
||||
@@ -23,5 +23,5 @@ class InsertLocation(Enum):
|
||||
"""
|
||||
|
||||
TOP = "Top"
|
||||
AFTER_TITLE = "Header"
|
||||
AFTER_TITLE = "After title"
|
||||
BOTTOM = "Bottom"
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
"""Work with metadata items."""
|
||||
|
||||
import copy
|
||||
import re
|
||||
from io import StringIO
|
||||
|
||||
from rich import print
|
||||
from rich.columns import Columns
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
from obsidian_metadata._utils.alerts import logger as log
|
||||
from obsidian_metadata._utils import alerts
|
||||
from obsidian_metadata._utils import (
|
||||
clean_dictionary,
|
||||
dict_contains,
|
||||
@@ -18,6 +15,7 @@ from obsidian_metadata._utils import (
|
||||
merge_dictionaries,
|
||||
remove_markdown_sections,
|
||||
)
|
||||
from obsidian_metadata._utils.console import console
|
||||
from obsidian_metadata.models import Patterns # isort: ignore
|
||||
from obsidian_metadata.models.enums import MetadataType
|
||||
|
||||
@@ -49,21 +47,19 @@ class VaultMetadata:
|
||||
"""
|
||||
if isinstance(metadata, dict):
|
||||
new_metadata = clean_dictionary(metadata)
|
||||
self.dict = merge_dictionaries(self.dict.copy(), new_metadata.copy())
|
||||
self.dict = merge_dictionaries(self.dict, new_metadata)
|
||||
|
||||
if area == MetadataType.FRONTMATTER:
|
||||
self.frontmatter = merge_dictionaries(self.frontmatter.copy(), new_metadata.copy())
|
||||
self.frontmatter = merge_dictionaries(self.frontmatter, new_metadata)
|
||||
|
||||
if area == MetadataType.INLINE:
|
||||
self.inline_metadata = merge_dictionaries(
|
||||
self.inline_metadata.copy(), new_metadata.copy()
|
||||
)
|
||||
self.inline_metadata = merge_dictionaries(self.inline_metadata, new_metadata)
|
||||
|
||||
if area == MetadataType.TAGS and isinstance(metadata, list):
|
||||
self.tags.extend(metadata)
|
||||
self.tags = sorted({s.strip("#") for s in self.tags})
|
||||
|
||||
def contains(
|
||||
def contains( # noqa: PLR0911
|
||||
self, area: MetadataType, key: str = None, value: str = None, is_regex: bool = False
|
||||
) -> bool:
|
||||
"""Check if a key and/or a value exists in the metadata.
|
||||
@@ -84,7 +80,7 @@ class VaultMetadata:
|
||||
if area != MetadataType.TAGS and key is None:
|
||||
raise ValueError("Key must be provided when checking for a key's existence.")
|
||||
|
||||
match area: # noqa: E999
|
||||
match area:
|
||||
case MetadataType.ALL:
|
||||
if dict_contains(self.dict, key, value, is_regex):
|
||||
return True
|
||||
@@ -118,7 +114,7 @@ class VaultMetadata:
|
||||
Returns:
|
||||
bool: True if a value was deleted
|
||||
"""
|
||||
new_dict = self.dict.copy()
|
||||
new_dict = copy.deepcopy(self.dict)
|
||||
|
||||
if value_to_delete is None:
|
||||
for _k in list(new_dict):
|
||||
@@ -175,7 +171,7 @@ class VaultMetadata:
|
||||
"\n".join(sorted(value)) if isinstance(value, list) else value
|
||||
)
|
||||
table.add_row(f"[bold]{key}[/]", str(values))
|
||||
Console().print(table)
|
||||
console.print(table)
|
||||
|
||||
if list_to_print is not None:
|
||||
columns = Columns(
|
||||
@@ -184,7 +180,7 @@ class VaultMetadata:
|
||||
expand=True,
|
||||
title=header if area != MetadataType.ALL else "All inline tags",
|
||||
)
|
||||
print(columns)
|
||||
console.print(columns)
|
||||
|
||||
def rename(self, key: str, value_1: str, value_2: str = None) -> bool:
|
||||
"""Replace a value in the frontmatter.
|
||||
@@ -216,7 +212,7 @@ class Frontmatter:
|
||||
|
||||
def __init__(self, file_content: str):
|
||||
self.dict: dict[str, list[str]] = self._grab_note_frontmatter(file_content)
|
||||
self.dict_original: dict[str, list[str]] = self.dict.copy()
|
||||
self.dict_original: dict[str, list[str]] = copy.deepcopy(self.dict)
|
||||
|
||||
def __repr__(self) -> str: # pragma: no cover
|
||||
"""Representation of the frontmatter.
|
||||
@@ -230,7 +226,7 @@ class Frontmatter:
|
||||
"""Grab metadata from a note.
|
||||
|
||||
Args:
|
||||
note_path (Path): Path to the note file.
|
||||
file_content (str): Content of the note.
|
||||
|
||||
Returns:
|
||||
dict: Metadata from the note.
|
||||
@@ -243,7 +239,11 @@ class Frontmatter:
|
||||
return {}
|
||||
|
||||
yaml = YAML(typ="safe")
|
||||
frontmatter: dict = yaml.load(frontmatter_block)
|
||||
yaml.allow_unicode = False
|
||||
try:
|
||||
frontmatter: dict = yaml.load(frontmatter_block)
|
||||
except Exception as e: # noqa: BLE001
|
||||
raise AttributeError(e) from e
|
||||
|
||||
for k in frontmatter:
|
||||
if frontmatter[k] is None:
|
||||
@@ -251,7 +251,7 @@ class Frontmatter:
|
||||
|
||||
return dict_values_to_lists_strings(frontmatter, strip_null_values=True)
|
||||
|
||||
def add(self, key: str, value: str | list[str] = None) -> bool:
|
||||
def add(self, key: str, value: str | list[str] = None) -> bool: # noqa: PLR0911
|
||||
"""Add a key and value to the frontmatter.
|
||||
|
||||
Args:
|
||||
@@ -364,7 +364,7 @@ class Frontmatter:
|
||||
str: Frontmatter as a YAML string.
|
||||
sort_keys (bool, optional): Sort the keys. Defaults to False.
|
||||
"""
|
||||
dict_to_dump = self.dict.copy()
|
||||
dict_to_dump = copy.deepcopy(self.dict)
|
||||
for k in dict_to_dump:
|
||||
if dict_to_dump[k] == []:
|
||||
dict_to_dump[k] = None
|
||||
@@ -391,7 +391,7 @@ class InlineMetadata:
|
||||
|
||||
def __init__(self, file_content: str):
|
||||
self.dict: dict[str, list[str]] = self.grab_inline_metadata(file_content)
|
||||
self.dict_original: dict[str, list[str]] = self.dict.copy()
|
||||
self.dict_original: dict[str, list[str]] = copy.deepcopy(self.dict)
|
||||
|
||||
def __repr__(self) -> str: # pragma: no cover
|
||||
"""Representation of inline metadata.
|
||||
@@ -401,7 +401,7 @@ class InlineMetadata:
|
||||
"""
|
||||
return f"InlineMetadata(inline_metadata={self.dict})"
|
||||
|
||||
def add(self, key: str, value: str = None) -> bool:
|
||||
def add(self, key: str, value: str | list[str] = None) -> bool: # noqa: PLR0911
|
||||
"""Add a key and value to the inline metadata.
|
||||
|
||||
Args:
|
||||
@@ -411,23 +411,29 @@ class InlineMetadata:
|
||||
Returns:
|
||||
bool: True if the metadata was added
|
||||
"""
|
||||
if value is None or value == "" or value == "None":
|
||||
if value is None:
|
||||
if key not in self.dict:
|
||||
self.dict[key] = []
|
||||
return True
|
||||
return False
|
||||
|
||||
if key not in self.dict:
|
||||
if isinstance(value, list):
|
||||
self.dict[key] = value
|
||||
return True
|
||||
|
||||
self.dict[key] = [value]
|
||||
return True
|
||||
|
||||
if key in self.dict and len(self.dict[key]) > 0:
|
||||
if value in self.dict[key]:
|
||||
return False
|
||||
raise ValueError(f"'{key}' not empty")
|
||||
if key in self.dict and value not in self.dict[key]:
|
||||
if isinstance(value, list):
|
||||
self.dict[key].extend(value)
|
||||
return True
|
||||
|
||||
self.dict[key].append(value)
|
||||
return True
|
||||
self.dict[key].append(value)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def contains(self, key: str, value: str = None, is_regex: bool = False) -> bool:
|
||||
"""Check if a key or value exists in the inline metadata.
|
||||
@@ -561,7 +567,7 @@ class InlineTags:
|
||||
)
|
||||
)
|
||||
|
||||
def add(self, new_tag: str) -> bool:
|
||||
def add(self, new_tag: str | list[str]) -> bool:
|
||||
"""Add a new inline tag.
|
||||
|
||||
Args:
|
||||
@@ -570,13 +576,27 @@ class InlineTags:
|
||||
Returns:
|
||||
bool: True if a tag was added.
|
||||
"""
|
||||
if new_tag in self.list:
|
||||
return False
|
||||
if isinstance(new_tag, list):
|
||||
for _tag in new_tag:
|
||||
if _tag.startswith("#"):
|
||||
_tag = _tag[1:]
|
||||
if _tag in self.list:
|
||||
return False
|
||||
new_list = self.list.copy()
|
||||
new_list.append(_tag)
|
||||
self.list = sorted(new_list)
|
||||
return True
|
||||
else:
|
||||
if new_tag.startswith("#"):
|
||||
new_tag = new_tag[1:]
|
||||
if new_tag in self.list:
|
||||
return False
|
||||
new_list = self.list.copy()
|
||||
new_list.append(new_tag)
|
||||
self.list = sorted(new_list)
|
||||
return True
|
||||
|
||||
new_list = self.list.copy()
|
||||
new_list.append(new_tag)
|
||||
self.list = sorted(new_list)
|
||||
return True
|
||||
return False
|
||||
|
||||
def contains(self, tag: str, is_regex: bool = False) -> bool:
|
||||
"""Check if a tag exists in the metadata.
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
"""Representation of notes and in the vault."""
|
||||
"""Representation of a not in the vault."""
|
||||
|
||||
|
||||
import copy
|
||||
import difflib
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import rich.repr
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
from obsidian_metadata._utils import alerts
|
||||
from obsidian_metadata._utils.alerts import logger as log
|
||||
from obsidian_metadata._utils.console import console
|
||||
from obsidian_metadata.models import (
|
||||
Frontmatter,
|
||||
InlineMetadata,
|
||||
@@ -51,7 +53,12 @@ class Note:
|
||||
alerts.error(f"Note {self.note_path} not found. Exiting")
|
||||
raise typer.Exit(code=1) from e
|
||||
|
||||
self.frontmatter: Frontmatter = Frontmatter(self.file_content)
|
||||
try:
|
||||
self.frontmatter: Frontmatter = Frontmatter(self.file_content)
|
||||
except AttributeError as e:
|
||||
alerts.error(f"Note {self.note_path} has invalid frontmatter.\n{e}")
|
||||
raise typer.Exit(code=1) from e
|
||||
|
||||
self.inline_tags: InlineTags = InlineTags(self.file_content)
|
||||
self.inline_metadata: InlineMetadata = InlineMetadata(self.file_content)
|
||||
self.original_file_content: str = self.file_content
|
||||
@@ -64,67 +71,14 @@ class Note:
|
||||
yield "inline_tags", self.inline_tags
|
||||
yield "inline_metadata", self.inline_metadata
|
||||
|
||||
def _delete_inline_metadata(self, key: str, value: str = None) -> None:
|
||||
"""Delete an inline metadata key/value pair from the text of the note. This method does not remove the key/value from the metadata attribute of the note.
|
||||
|
||||
Args:
|
||||
key (str): Key to delete.
|
||||
value (str, optional): Value to delete.
|
||||
"""
|
||||
all_results = PATTERNS.find_inline_metadata.findall(self.file_content)
|
||||
stripped_null_values = [tuple(filter(None, x)) for x in all_results]
|
||||
|
||||
for _k, _v in stripped_null_values:
|
||||
if re.search(key, _k):
|
||||
if value is None:
|
||||
_k = re.escape(_k)
|
||||
_v = re.escape(_v)
|
||||
self.sub(rf"\[?{_k}:: ?{_v}]?", "", is_regex=True)
|
||||
return
|
||||
|
||||
if re.search(value, _v):
|
||||
_k = re.escape(_k)
|
||||
_v = re.escape(_v)
|
||||
self.sub(rf"({_k}::) ?{_v}", r"\1", is_regex=True)
|
||||
|
||||
def _rename_inline_metadata(self, key: str, value_1: str, value_2: str = None) -> None:
|
||||
"""Replace the inline metadata in the note with the current inline metadata object.
|
||||
|
||||
Args:
|
||||
key (str): Key to rename.
|
||||
value_1 (str): Value to replace OR new key name (if value_2 is None).
|
||||
value_2 (str, optional): New value.
|
||||
|
||||
"""
|
||||
all_results = PATTERNS.find_inline_metadata.findall(self.file_content)
|
||||
stripped_null_values = [tuple(filter(None, x)) for x in all_results]
|
||||
|
||||
for _k, _v in stripped_null_values:
|
||||
if re.search(key, _k):
|
||||
if value_2 is None:
|
||||
if re.search(rf"{key}[^\w\d_-]+", _k):
|
||||
key_text = re.split(r"[^\w\d_-]+$", _k)[0]
|
||||
key_markdown = re.split(r"^[\w\d_-]+", _k)[1]
|
||||
self.sub(
|
||||
rf"{key_text}{key_markdown}::",
|
||||
rf"{value_1}{key_markdown}::",
|
||||
)
|
||||
else:
|
||||
self.sub(f"{_k}::", f"{value_1}::")
|
||||
else:
|
||||
if re.search(key, _k) and re.search(value_1, _v):
|
||||
_k = re.escape(_k)
|
||||
_v = re.escape(_v)
|
||||
self.sub(f"{_k}:: ?{_v}", f"{_k}:: {value_2}", is_regex=True)
|
||||
|
||||
def add_metadata(
|
||||
def add_metadata( # noqa: C901
|
||||
self,
|
||||
area: MetadataType,
|
||||
key: str = None,
|
||||
value: str | list[str] = None,
|
||||
location: InsertLocation = None,
|
||||
) -> bool:
|
||||
"""Add metadata to the note if it does not already exist.
|
||||
"""Add metadata to the note if it does not already exist. This method adds specified metadata to the appropriate MetadataType object AND writes the new metadata to the note's file.
|
||||
|
||||
Args:
|
||||
area (MetadataType): Area to add metadata to.
|
||||
@@ -135,27 +89,70 @@ class Note:
|
||||
Returns:
|
||||
bool: Whether the metadata was added.
|
||||
"""
|
||||
if area is MetadataType.FRONTMATTER and self.frontmatter.add(key, value):
|
||||
self.update_frontmatter()
|
||||
return True
|
||||
|
||||
try:
|
||||
if area is MetadataType.INLINE and self.inline_metadata.add(key, str(value)):
|
||||
line = f"{key}:: " if value is None else f"{key}:: {value}"
|
||||
self.insert(new_string=line, location=location)
|
||||
match area:
|
||||
case MetadataType.FRONTMATTER if self.frontmatter.add(key, value):
|
||||
self.write_frontmatter()
|
||||
return True
|
||||
|
||||
except ValueError as e:
|
||||
log.warning(f"Could not add metadata to {self.note_path}: {e}")
|
||||
return False
|
||||
case MetadataType.INLINE:
|
||||
if value is None and self.inline_metadata.add(key):
|
||||
line = f"{key}::"
|
||||
self.write_string(new_string=line, location=location)
|
||||
return True
|
||||
|
||||
if area is MetadataType.TAGS and self.inline_tags.add(str(value)):
|
||||
line = f"#{value}"
|
||||
self.insert(new_string=line, location=location)
|
||||
return True
|
||||
new_values = []
|
||||
if isinstance(value, list):
|
||||
new_values = [_v for _v in value if self.inline_metadata.add(key, _v)]
|
||||
elif self.inline_metadata.add(key, value):
|
||||
new_values = [value]
|
||||
|
||||
if new_values:
|
||||
for value in new_values:
|
||||
self.write_string(new_string=f"{key}:: {value}", location=location)
|
||||
return True
|
||||
|
||||
case MetadataType.TAGS:
|
||||
new_values = []
|
||||
if isinstance(value, list):
|
||||
new_values = [_v for _v in value if self.inline_tags.add(_v)]
|
||||
elif self.inline_tags.add(value):
|
||||
new_values = [value]
|
||||
|
||||
if new_values:
|
||||
for value in new_values:
|
||||
_v = value
|
||||
if _v.startswith("#"):
|
||||
_v = _v[1:]
|
||||
self.write_string(new_string=f"#{_v}", location=location)
|
||||
return True
|
||||
|
||||
case _:
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
def commit(self, path: Path = None) -> None:
|
||||
"""Write the note's new content to disk. This is a destructive action.
|
||||
|
||||
Args:
|
||||
path (Path): Path to write the note to. Defaults to the note's path.
|
||||
|
||||
Raises:
|
||||
typer.Exit: If the note's path is not found.
|
||||
"""
|
||||
p = self.note_path if path is None else path
|
||||
if self.dry_run:
|
||||
log.trace(f"DRY RUN: Writing note {p} to disk")
|
||||
return
|
||||
|
||||
try:
|
||||
with open(p, "w") as f:
|
||||
log.trace(f"Writing note {p} to disk")
|
||||
f.write(self.file_content)
|
||||
except FileNotFoundError as e:
|
||||
alerts.error(f"Note {p} not found. Exiting")
|
||||
raise typer.Exit(code=1) from e
|
||||
|
||||
def contains_inline_tag(self, tag: str, is_regex: bool = False) -> bool:
|
||||
"""Check if a note contains the specified inline tag.
|
||||
|
||||
@@ -169,7 +166,7 @@ class Note:
|
||||
return self.inline_tags.contains(tag, is_regex=is_regex)
|
||||
|
||||
def contains_metadata(self, key: str, value: str = None, is_regex: bool = False) -> bool:
|
||||
"""Check if a note has a key or a key-value pair in its metadata.
|
||||
"""Check if a note has a key or a key-value pair in its Frontmatter or InlineMetadata.
|
||||
|
||||
Args:
|
||||
key (str): Key to check for.
|
||||
@@ -215,34 +212,35 @@ class Note:
|
||||
|
||||
return False
|
||||
|
||||
def delete_metadata(self, key: str, value: str = None) -> bool:
|
||||
"""Delete a key or key-value pair from the note's metadata. Regex is supported.
|
||||
def delete_metadata(
|
||||
self, key: str, value: str = None, area: MetadataType = MetadataType.ALL
|
||||
) -> bool:
|
||||
"""Delete a key or key-value pair from the note's Metadata object and the content of the note. Regex is supported.
|
||||
|
||||
If no value is provided, will delete an entire key.
|
||||
If no value is provided, will delete an entire specified key.
|
||||
|
||||
Args:
|
||||
key (str): Key to delete.
|
||||
value (str, optional): Value to delete.
|
||||
area (MetadataType, optional): Area to delete metadata from. Defaults to MetadataType.ALL.
|
||||
|
||||
Returns:
|
||||
bool: Whether the key or key-value pair was deleted.
|
||||
"""
|
||||
changed_value: bool = False
|
||||
|
||||
if value is None:
|
||||
if self.frontmatter.delete(key):
|
||||
self.update_frontmatter()
|
||||
changed_value = True
|
||||
if self.inline_metadata.delete(key):
|
||||
self._delete_inline_metadata(key, value)
|
||||
changed_value = True
|
||||
else:
|
||||
if self.frontmatter.delete(key, value):
|
||||
self.update_frontmatter()
|
||||
changed_value = True
|
||||
if self.inline_metadata.delete(key, value):
|
||||
self._delete_inline_metadata(key, value)
|
||||
changed_value = True
|
||||
if (
|
||||
area == MetadataType.FRONTMATTER or area == MetadataType.ALL
|
||||
) and self.frontmatter.delete(key, value):
|
||||
self.write_frontmatter()
|
||||
changed_value = True
|
||||
|
||||
if (
|
||||
area == MetadataType.INLINE or area == MetadataType.ALL
|
||||
) and self.inline_metadata.contains(key, value):
|
||||
self.write_delete_inline_metadata(key, value)
|
||||
self.inline_metadata.delete(key, value)
|
||||
changed_value = True
|
||||
|
||||
if changed_value:
|
||||
return True
|
||||
@@ -268,59 +266,8 @@ class Note:
|
||||
|
||||
return False
|
||||
|
||||
def insert(
|
||||
self,
|
||||
new_string: str,
|
||||
location: InsertLocation,
|
||||
allow_multiple: bool = False,
|
||||
) -> None:
|
||||
"""Insert a string at the top of a note.
|
||||
|
||||
Args:
|
||||
new_string (str): String to insert at the top of the note.
|
||||
allow_multiple (bool): Whether to allow inserting the string if it already exists in the note.
|
||||
location (InsertLocation): Location to insert the string.
|
||||
"""
|
||||
if not allow_multiple and len(re.findall(re.escape(new_string), self.file_content)) > 0:
|
||||
return
|
||||
|
||||
match location: # noqa: E999
|
||||
case InsertLocation.BOTTOM:
|
||||
self.file_content += f"\n{new_string}"
|
||||
case InsertLocation.TOP:
|
||||
try:
|
||||
top = PATTERNS.frontmatter_block.search(self.file_content).group("frontmatter")
|
||||
except AttributeError:
|
||||
top = ""
|
||||
|
||||
if top == "":
|
||||
self.file_content = f"{new_string}\n{self.file_content}"
|
||||
else:
|
||||
new_string = f"{top}\n{new_string}"
|
||||
top = re.escape(top)
|
||||
self.sub(top, new_string, is_regex=True)
|
||||
case InsertLocation.AFTER_TITLE:
|
||||
try:
|
||||
top = PATTERNS.top_with_header.search(self.file_content).group("top")
|
||||
except AttributeError:
|
||||
top = ""
|
||||
|
||||
if top == "":
|
||||
self.file_content = f"{new_string}\n{self.file_content}"
|
||||
else:
|
||||
new_string = f"{top}\n{new_string}"
|
||||
top = re.escape(top)
|
||||
self.sub(top, new_string, is_regex=True)
|
||||
case _:
|
||||
raise ValueError(f"Invalid location: {location}")
|
||||
pass
|
||||
|
||||
def print_note(self) -> None:
|
||||
"""Print the note to the console."""
|
||||
print(self.file_content)
|
||||
|
||||
def print_diff(self) -> None:
|
||||
"""Print a diff of the note's original state and it's new state."""
|
||||
"""Print a diff of the note's content. Compares original state to it's new state."""
|
||||
a = self.original_file_content.splitlines()
|
||||
b = self.file_content.splitlines()
|
||||
|
||||
@@ -334,10 +281,14 @@ class Note:
|
||||
elif line.startswith("-"):
|
||||
table.add_row(line, style="red")
|
||||
|
||||
Console().print(table)
|
||||
console.print(table)
|
||||
|
||||
def print_note(self) -> None:
|
||||
"""Print the note to the console."""
|
||||
console.print(self.file_content)
|
||||
|
||||
def rename_inline_tag(self, tag_1: str, tag_2: str) -> bool:
|
||||
"""Rename an inline tag from the note ONLY if it's not in the metadata as well.
|
||||
"""Rename an inline tag. Updates the Metadata object and the text of the note.
|
||||
|
||||
Args:
|
||||
tag_1 (str): Tag to rename.
|
||||
@@ -357,9 +308,9 @@ class Note:
|
||||
return False
|
||||
|
||||
def rename_metadata(self, key: str, value_1: str, value_2: str = None) -> bool:
|
||||
"""Rename a key or key-value pair in the note's metadata.
|
||||
"""Rename a key or key-value pair in the note's InlineMetadata and Frontmatter objects and the content of the note.
|
||||
|
||||
If no value is provided, will rename an entire key.
|
||||
If no value is provided, will rename the entire specified key.
|
||||
|
||||
Args:
|
||||
key (str): Key to rename.
|
||||
@@ -372,17 +323,17 @@ class Note:
|
||||
changed_value: bool = False
|
||||
if value_2 is None:
|
||||
if self.frontmatter.rename(key, value_1):
|
||||
self.update_frontmatter()
|
||||
self.write_frontmatter()
|
||||
changed_value = True
|
||||
if self.inline_metadata.rename(key, value_1):
|
||||
self._rename_inline_metadata(key, value_1)
|
||||
self.write_inline_metadata_change(key, value_1)
|
||||
changed_value = True
|
||||
else:
|
||||
if self.frontmatter.rename(key, value_1, value_2):
|
||||
self.update_frontmatter()
|
||||
self.write_frontmatter()
|
||||
changed_value = True
|
||||
if self.inline_metadata.rename(key, value_1, value_2):
|
||||
self._rename_inline_metadata(key, value_1, value_2)
|
||||
self.write_inline_metadata_change(key, value_1, value_2)
|
||||
changed_value = True
|
||||
|
||||
if changed_value:
|
||||
@@ -403,8 +354,122 @@ class Note:
|
||||
|
||||
self.file_content = re.sub(pattern, replacement, self.file_content, re.MULTILINE)
|
||||
|
||||
def update_frontmatter(self, sort_keys: bool = False) -> None:
|
||||
"""Replace the frontmatter in the note with the current frontmatter object."""
|
||||
def transpose_metadata( # noqa: C901, PLR0912, PLR0911, PLR0913
|
||||
self,
|
||||
begin: MetadataType,
|
||||
end: MetadataType,
|
||||
key: str = None,
|
||||
value: str | list[str] = None,
|
||||
location: InsertLocation = InsertLocation.BOTTOM,
|
||||
) -> bool:
|
||||
"""Move metadata from one metadata object to another. i.e. Frontmatter to InlineMetadata or vice versa.
|
||||
|
||||
If no key is specified, will transpose all metadata. If a key is specified, but no value, the entire key will be transposed. if a specific value is specified, just that value will be transposed.
|
||||
|
||||
Args:
|
||||
begin (MetadataType): The type of metadata to transpose from.
|
||||
end (MetadataType): The type of metadata to transpose to.
|
||||
key (str, optional): The key to transpose. Defaults to None.
|
||||
location (InsertLocation, optional): Where to insert the metadata. Defaults to InsertLocation.BOTTOM.
|
||||
value (str | list[str], optional): The value to transpose. Defaults to None.
|
||||
|
||||
Returns:
|
||||
bool: Whether the note was updated.
|
||||
"""
|
||||
if (begin == MetadataType.FRONTMATTER or begin == MetadataType.INLINE) and (
|
||||
end == MetadataType.FRONTMATTER or end == MetadataType.INLINE
|
||||
):
|
||||
if begin == MetadataType.FRONTMATTER:
|
||||
begin_dict = self.frontmatter.dict
|
||||
else:
|
||||
begin_dict = self.inline_metadata.dict
|
||||
|
||||
if begin_dict == {}:
|
||||
return False
|
||||
|
||||
if key is None: # Transpose all metadata when no key is provided
|
||||
for _key, _value in begin_dict.items():
|
||||
self.add_metadata(key=_key, value=_value, area=end, location=location)
|
||||
self.delete_metadata(key=_key, area=begin)
|
||||
return True
|
||||
|
||||
has_changes = False
|
||||
temp_dict = copy.deepcopy(begin_dict)
|
||||
for k, v in begin_dict.items():
|
||||
if key == k:
|
||||
if value is None:
|
||||
self.add_metadata(key=k, value=v, area=end, location=location)
|
||||
self.delete_metadata(key=k, area=begin)
|
||||
return True
|
||||
|
||||
if value == v:
|
||||
self.add_metadata(key=k, value=v, area=end, location=location)
|
||||
self.delete_metadata(key=k, area=begin)
|
||||
return True
|
||||
|
||||
if isinstance(value, str):
|
||||
if value in v:
|
||||
self.add_metadata(key=k, value=value, area=end, location=location)
|
||||
self.delete_metadata(key=k, value=value, area=begin)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
if isinstance(value, list):
|
||||
for value_item in value:
|
||||
if value_item in v:
|
||||
self.add_metadata(
|
||||
key=k, value=value_item, area=end, location=location
|
||||
)
|
||||
self.delete_metadata(key=k, value=value_item, area=begin)
|
||||
temp_dict[k].remove(value_item)
|
||||
has_changes = True
|
||||
|
||||
if temp_dict[k] == []:
|
||||
self.delete_metadata(key=k, area=begin)
|
||||
|
||||
return bool(has_changes)
|
||||
|
||||
if begin == MetadataType.TAGS:
|
||||
# TODO: Implement transposing to and from tags
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
def write_delete_inline_metadata(self, key: str = None, value: str = None) -> bool:
|
||||
"""For a given inline metadata key and/or key-value pair, delete it from the text of the note. If no key is provided, will delete all inline metadata from the text of the note.
|
||||
|
||||
IMPORTANT: This method makes no changes to the InlineMetadata object.
|
||||
|
||||
Args:
|
||||
key (str, optional): Key to delete.
|
||||
value (str, optional): Value to delete.
|
||||
|
||||
Returns:
|
||||
bool: Whether the note was updated.
|
||||
"""
|
||||
for _k, _v in self.inline_metadata.dict.items():
|
||||
if re.search(key, _k):
|
||||
for _value in _v:
|
||||
if value is None:
|
||||
_k = re.escape(_k)
|
||||
_value = re.escape(_value)
|
||||
self.sub(rf"\[?{_k}:: ?{_value}]?", "", is_regex=True)
|
||||
return True
|
||||
|
||||
if re.search(value, _value):
|
||||
_k = re.escape(_k)
|
||||
_value = re.escape(_value)
|
||||
self.sub(rf"({_k}::) ?{_value}", r"\1", is_regex=True)
|
||||
return True
|
||||
return False
|
||||
|
||||
def write_frontmatter(self, sort_keys: bool = False) -> bool:
|
||||
"""Replace the frontmatter in the note with the current Frontmatter object. If the Frontmatter object is empty, will delete the frontmatter from the note.
|
||||
|
||||
Returns:
|
||||
bool: Whether the note was updated.
|
||||
"""
|
||||
try:
|
||||
current_frontmatter = PATTERNS.frontmatter_block.search(self.file_content).group(
|
||||
"frontmatter"
|
||||
@@ -413,36 +478,121 @@ class Note:
|
||||
current_frontmatter = None
|
||||
|
||||
if current_frontmatter is None and self.frontmatter.dict == {}:
|
||||
return
|
||||
return False
|
||||
|
||||
new_frontmatter = self.frontmatter.to_yaml(sort_keys=sort_keys)
|
||||
new_frontmatter = f"---\n{new_frontmatter}---\n"
|
||||
new_frontmatter = "" if self.frontmatter.dict == {} else f"---\n{new_frontmatter}---\n"
|
||||
|
||||
if current_frontmatter is None:
|
||||
self.file_content = new_frontmatter + self.file_content
|
||||
return
|
||||
return True
|
||||
|
||||
current_frontmatter = re.escape(current_frontmatter)
|
||||
current_frontmatter = f"{re.escape(current_frontmatter)}\n?"
|
||||
self.sub(current_frontmatter, new_frontmatter, is_regex=True)
|
||||
return True
|
||||
|
||||
def write(self, path: Path = None) -> None:
|
||||
"""Write the note's content to disk.
|
||||
def write_all_inline_metadata(
|
||||
self,
|
||||
location: InsertLocation,
|
||||
) -> bool:
|
||||
"""Write all metadata found in the InlineMetadata object to the note at a specified insert location.
|
||||
|
||||
Args:
|
||||
path (Path): Path to write the note to. Defaults to the note's path.
|
||||
location (InsertLocation): Where to insert the metadata.
|
||||
|
||||
Raises:
|
||||
typer.Exit: If the note's path is not found.
|
||||
Returns:
|
||||
bool: Whether the note was updated.
|
||||
"""
|
||||
p = self.note_path if path is None else path
|
||||
if self.dry_run:
|
||||
log.trace(f"DRY RUN: Writing note {p} to disk")
|
||||
return
|
||||
if self.inline_metadata.dict != {}:
|
||||
string = ""
|
||||
for k, v in sorted(self.inline_metadata.dict.items()):
|
||||
for value in v:
|
||||
string += f"{k}:: {value}\n"
|
||||
|
||||
try:
|
||||
with open(p, "w") as f:
|
||||
log.trace(f"Writing note {p} to disk")
|
||||
f.write(self.file_content)
|
||||
except FileNotFoundError as e:
|
||||
alerts.error(f"Note {p} not found. Exiting")
|
||||
raise typer.Exit(code=1) from e
|
||||
if self.write_string(new_string=string, location=location, allow_multiple=True):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def write_inline_metadata_change(self, key: str, value_1: str, value_2: str = None) -> None:
|
||||
"""Write changes to a specific inline metadata key or value.
|
||||
|
||||
Args:
|
||||
key (str): Key to rename.
|
||||
value_1 (str): Value to replace OR new key name (if value_2 is None).
|
||||
value_2 (str, optional): New value.
|
||||
|
||||
"""
|
||||
all_results = PATTERNS.find_inline_metadata.findall(self.file_content)
|
||||
stripped_null_values = [tuple(filter(None, x)) for x in all_results]
|
||||
|
||||
for _k, _v in stripped_null_values:
|
||||
if re.search(key, _k):
|
||||
if value_2 is None:
|
||||
if re.search(rf"{key}[^\\w\\d_-]+", _k):
|
||||
key_text = re.split(r"[^\\w\\d_-]+$", _k)[0]
|
||||
key_markdown = re.split(r"^[\\w\\d_-]+", _k)[1]
|
||||
self.sub(
|
||||
rf"{key_text}{key_markdown}::",
|
||||
rf"{value_1}{key_markdown}::",
|
||||
)
|
||||
else:
|
||||
self.sub(f"{_k}::", f"{value_1}::")
|
||||
elif re.search(key, _k) and re.search(value_1, _v):
|
||||
_k = re.escape(_k)
|
||||
_v = re.escape(_v)
|
||||
self.sub(f"{_k}:: ?{_v}", f"{_k}:: {value_2}", is_regex=True)
|
||||
|
||||
def write_string(
|
||||
self,
|
||||
new_string: str,
|
||||
location: InsertLocation,
|
||||
allow_multiple: bool = False,
|
||||
) -> bool:
|
||||
"""Insert a string into the note at a requested location.
|
||||
|
||||
Args:
|
||||
new_string (str): String to insert at the top of the note.
|
||||
allow_multiple (bool): Whether to allow inserting the string if it already exists in the note.
|
||||
location (InsertLocation): Location to insert the string.
|
||||
|
||||
Returns:
|
||||
bool: Whether the note was updated.
|
||||
"""
|
||||
if not allow_multiple and len(re.findall(re.escape(new_string), self.file_content)) > 0:
|
||||
return False
|
||||
|
||||
match location:
|
||||
case InsertLocation.BOTTOM:
|
||||
self.file_content += f"\n{new_string}"
|
||||
return True
|
||||
case InsertLocation.TOP:
|
||||
try:
|
||||
top = PATTERNS.frontmatter_block.search(self.file_content).group("frontmatter")
|
||||
except AttributeError:
|
||||
top = ""
|
||||
|
||||
if top == "":
|
||||
self.file_content = f"{new_string}\n{self.file_content}"
|
||||
return True
|
||||
|
||||
new_string = f"{top}\n{new_string}"
|
||||
top = re.escape(top)
|
||||
self.sub(top, new_string, is_regex=True)
|
||||
return True
|
||||
case InsertLocation.AFTER_TITLE:
|
||||
try:
|
||||
top = PATTERNS.top_with_header.search(self.file_content).group("top")
|
||||
except AttributeError:
|
||||
top = ""
|
||||
|
||||
if top == "":
|
||||
self.file_content = f"{new_string}\n{self.file_content}"
|
||||
return True
|
||||
|
||||
new_string = f"{top}\n{new_string}"
|
||||
top = re.escape(top)
|
||||
self.sub(top, new_string, is_regex=True)
|
||||
return True
|
||||
case _: # pragma: no cover
|
||||
raise ValueError(f"Invalid location: {location}")
|
||||
|
||||
@@ -274,12 +274,15 @@ class Questions:
|
||||
return questionary.select(
|
||||
"What do you want to do?",
|
||||
choices=[
|
||||
questionary.Separator("-------------------------------"),
|
||||
{"name": "Vault Actions", "value": "vault_actions"},
|
||||
{"name": "Inspect Metadata", "value": "inspect_metadata"},
|
||||
{"name": "Filter Notes in Scope", "value": "filter_notes"},
|
||||
questionary.Separator("-------------------------------"),
|
||||
{"name": "Add Metadata", "value": "add_metadata"},
|
||||
{"name": "Rename Metadata", "value": "rename_metadata"},
|
||||
{"name": "Delete Metadata", "value": "delete_metadata"},
|
||||
{"name": "Rename Metadata", "value": "rename_metadata"},
|
||||
{"name": "Transpose Metadata", "value": "transpose_metadata"},
|
||||
questionary.Separator("-------------------------------"),
|
||||
{"name": "Review Changes", "value": "review_changes"},
|
||||
{"name": "Commit Changes", "value": "commit_changes"},
|
||||
@@ -495,6 +498,7 @@ class Questions:
|
||||
Returns:
|
||||
any: The selected item value.
|
||||
"""
|
||||
choices.insert(0, questionary.Separator())
|
||||
return questionary.select(
|
||||
question,
|
||||
choices=choices,
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
"""Obsidian vault representation."""
|
||||
|
||||
import csv
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
import rich.repr
|
||||
from rich import box
|
||||
from rich.console import Console
|
||||
from rich.progress import Progress, SpinnerColumn, TextColumn
|
||||
from rich.prompt import Confirm
|
||||
from rich.table import Table
|
||||
|
||||
from obsidian_metadata._config.config import Config, VaultConfig
|
||||
from obsidian_metadata._config.config import VaultConfig
|
||||
from obsidian_metadata._utils import alerts
|
||||
from obsidian_metadata._utils.alerts import logger as log
|
||||
from obsidian_metadata._utils.console import console
|
||||
from obsidian_metadata.models import InsertLocation, MetadataType, Note, VaultMetadata
|
||||
|
||||
|
||||
@@ -81,6 +82,7 @@ class Vault:
|
||||
yield "num_notes", len(self.all_notes)
|
||||
yield "num_notes_in_scope", len(self.notes_in_scope)
|
||||
yield "exclude_paths", self.exclude_paths
|
||||
yield "insert_location", self.insert_location
|
||||
|
||||
def _filter_notes(self) -> list[Note]:
|
||||
"""Filter notes by path and metadata using the filters defined in self.filters.
|
||||
@@ -113,20 +115,40 @@ class Vault:
|
||||
return notes_list
|
||||
|
||||
def _find_insert_location(self) -> InsertLocation:
|
||||
"""Find the insert location for a note.
|
||||
"""Find the insert location for a note from the configuration file.
|
||||
|
||||
Returns:
|
||||
InsertLocation: Insert location for the note.
|
||||
"""
|
||||
if self.config["insert_location"].upper() == "TOP":
|
||||
return InsertLocation.TOP
|
||||
elif self.config["insert_location"].upper() == "HEADER":
|
||||
|
||||
if self.config["insert_location"].upper() == "AFTER_TITLE":
|
||||
return InsertLocation.AFTER_TITLE
|
||||
elif self.config["insert_location"].upper() == "BOTTOM":
|
||||
return InsertLocation.BOTTOM
|
||||
else:
|
||||
|
||||
if self.config["insert_location"].upper() == "BOTTOM":
|
||||
return InsertLocation.BOTTOM
|
||||
|
||||
return InsertLocation.BOTTOM
|
||||
|
||||
@property
|
||||
def insert_location(self) -> InsertLocation:
|
||||
"""Location to insert new or reorganized metadata.
|
||||
|
||||
Returns:
|
||||
InsertLocation: The insert location.
|
||||
"""
|
||||
return self._insert_location
|
||||
|
||||
@insert_location.setter
|
||||
def insert_location(self, value: InsertLocation) -> None:
|
||||
"""Set the insert location for the vault.
|
||||
|
||||
Args:
|
||||
value (InsertLocation): The insert location to set.
|
||||
"""
|
||||
self._insert_location = value
|
||||
|
||||
def _find_markdown_notes(self) -> list[Path]:
|
||||
"""Build list of all markdown files in the vault.
|
||||
|
||||
@@ -199,7 +221,7 @@ class Vault:
|
||||
log.debug("Backing up vault")
|
||||
if self.dry_run:
|
||||
alerts.dryrun(f"Backup up vault to: {self.backup_path}")
|
||||
print("\n")
|
||||
console.print("\n")
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -231,7 +253,7 @@ class Vault:
|
||||
for _note in self.notes_in_scope:
|
||||
if _note.has_changes():
|
||||
log.trace(f"writing to {_note.note_path}")
|
||||
_note.write()
|
||||
_note.commit()
|
||||
|
||||
def delete_backup(self) -> None:
|
||||
"""Delete the vault backup."""
|
||||
@@ -285,16 +307,16 @@ class Vault:
|
||||
|
||||
return num_changed
|
||||
|
||||
def export_metadata(self, path: str, format: str = "csv") -> None:
|
||||
def export_metadata(self, path: str, export_format: str = "csv") -> None:
|
||||
"""Write metadata to a csv file.
|
||||
|
||||
Args:
|
||||
path (Path): Path to write csv file to.
|
||||
export_as (str, optional): Export as 'csv' or 'json'. Defaults to "csv".
|
||||
export_format (str, optional): Export as 'csv' or 'json'. Defaults to "csv".
|
||||
"""
|
||||
export_file = Path(path).expanduser().resolve()
|
||||
|
||||
match format: # noqa: E999
|
||||
match export_format:
|
||||
case "csv":
|
||||
with open(export_file, "w", encoding="UTF8") as f:
|
||||
writer = csv.writer(f)
|
||||
@@ -353,15 +375,16 @@ class Vault:
|
||||
table.add_row("Notes excluded from scope", str(self.num_excluded_notes()))
|
||||
table.add_row("Active filters", str(len(self.filters)))
|
||||
table.add_row("Notes with changes", str(len(self.get_changed_notes())))
|
||||
table.add_row("Insert Location", str(self.insert_location.value))
|
||||
|
||||
Console().print(table)
|
||||
console.print(table)
|
||||
|
||||
def list_editable_notes(self) -> None:
|
||||
"""Print a list of notes within the scope that are being edited."""
|
||||
table = Table(title="Notes in current scope", show_header=False, box=box.HORIZONTALS)
|
||||
for _n, _note in enumerate(self.notes_in_scope, start=1):
|
||||
table.add_row(str(_n), str(_note.note_path.relative_to(self.vault_path)))
|
||||
Console().print(table)
|
||||
console.print(table)
|
||||
|
||||
def num_excluded_notes(self) -> int:
|
||||
"""Count number of excluded notes."""
|
||||
@@ -411,3 +434,42 @@ class Vault:
|
||||
self._rebuild_vault_metadata()
|
||||
|
||||
return num_changed
|
||||
|
||||
def transpose_metadata( # noqa: PLR0913
|
||||
self,
|
||||
begin: MetadataType,
|
||||
end: MetadataType,
|
||||
key: str = None,
|
||||
value: str | list[str] = None,
|
||||
location: InsertLocation = None,
|
||||
) -> int:
|
||||
"""Transpose metadata from one type to another.
|
||||
|
||||
Args:
|
||||
begin (MetadataType): Metadata type to transpose from.
|
||||
end (MetadataType): Metadata type to transpose to.
|
||||
key (str, optional): Key to transpose. Defaults to None.
|
||||
value (str, optional): Value to transpose. Defaults to None.
|
||||
location (InsertLocation, optional): Location to insert metadata. (Defaults to `vault.config.insert_location`)
|
||||
|
||||
Returns:
|
||||
int: Number of notes that had metadata transposed.
|
||||
"""
|
||||
if location is None:
|
||||
location = self.insert_location
|
||||
|
||||
num_changed = 0
|
||||
for _note in self.notes_in_scope:
|
||||
if _note.transpose_metadata(
|
||||
begin=begin,
|
||||
end=end,
|
||||
key=key,
|
||||
value=value,
|
||||
location=location,
|
||||
):
|
||||
num_changed += 1
|
||||
|
||||
if num_changed > 0:
|
||||
self._rebuild_vault_metadata()
|
||||
|
||||
return num_changed
|
||||
|
||||
@@ -13,7 +13,7 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
from obsidian_metadata.models.enums import MetadataType
|
||||
from tests.helpers import Regex
|
||||
from tests.helpers import Regex, remove_ansi
|
||||
|
||||
|
||||
def test_instantiate_application(test_application) -> None:
|
||||
@@ -38,8 +38,8 @@ def test_abort(test_application, mocker, capsys) -> None:
|
||||
)
|
||||
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert "Done!" in captured.out
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert "Done!" in captured
|
||||
|
||||
|
||||
def test_add_metadata_frontmatter(test_application, mocker, capsys) -> None:
|
||||
@@ -65,8 +65,8 @@ def test_add_metadata_frontmatter(test_application, mocker, capsys) -> None:
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"SUCCESS +\| Added metadata to.*\d+.*notes", re.DOTALL)
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert captured == Regex(r"SUCCESS +\| Added metadata to \d+ notes", re.DOTALL)
|
||||
|
||||
|
||||
def test_add_metadata_inline(test_application, mocker, capsys) -> None:
|
||||
@@ -92,8 +92,8 @@ def test_add_metadata_inline(test_application, mocker, capsys) -> None:
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"SUCCESS +\| Added metadata to.*\d+.*notes", re.DOTALL)
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert captured == Regex(r"SUCCESS +\| Added metadata to \d+ notes", re.DOTALL)
|
||||
|
||||
|
||||
def test_add_metadata_tag(test_application, mocker, capsys) -> None:
|
||||
@@ -115,8 +115,8 @@ def test_add_metadata_tag(test_application, mocker, capsys) -> None:
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"SUCCESS +\| Added metadata to.*\d+.*notes", re.DOTALL)
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert captured == Regex(r"SUCCESS +\| Added metadata to \d+ notes", re.DOTALL)
|
||||
|
||||
|
||||
def test_delete_inline_tag(test_application, mocker, capsys) -> None:
|
||||
@@ -138,8 +138,8 @@ def test_delete_inline_tag(test_application, mocker, capsys) -> None:
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"WARNING +\| No notes were changed", re.DOTALL)
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert "WARNING | No notes were changed" in captured
|
||||
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
@@ -156,8 +156,8 @@ def test_delete_inline_tag(test_application, mocker, capsys) -> None:
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"SUCCESS +\| Deleted.*\d+.*notes", re.DOTALL)
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert captured == Regex(r"SUCCESS +\| Deleted inline tag: breakfast in \d+ notes", re.DOTALL)
|
||||
|
||||
|
||||
def test_delete_key(test_application, mocker, capsys) -> None:
|
||||
@@ -179,8 +179,8 @@ def test_delete_key(test_application, mocker, capsys) -> None:
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"WARNING +\| No notes found with a.*key.*matching", re.DOTALL)
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert r"WARNING | No notes found with a key matching: \d{7}" in captured
|
||||
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
@@ -197,10 +197,8 @@ def test_delete_key(test_application, mocker, capsys) -> None:
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(
|
||||
r"SUCCESS +\|.*Deleted.*keys.*matching:.*d\\w\+.*from.*10", re.DOTALL
|
||||
)
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert captured == Regex(r"SUCCESS \| Deleted keys matching: d\\w\+ from \d+ notes", re.DOTALL)
|
||||
|
||||
|
||||
def test_delete_value(test_application, mocker, capsys) -> None:
|
||||
@@ -225,8 +223,8 @@ def test_delete_value(test_application, mocker, capsys) -> None:
|
||||
)
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"WARNING +\| No notes found matching:", re.DOTALL)
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert r"WARNING | No notes found matching: area: \d{7}" in captured
|
||||
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
@@ -246,10 +244,8 @@ def test_delete_value(test_application, mocker, capsys) -> None:
|
||||
)
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(
|
||||
r"SUCCESS +\| Deleted value.*\^front\\w\+\$.*from.*key.*area.*in.*\d+.*notes", re.DOTALL
|
||||
)
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert r"SUCCESS | Deleted value ^front\w+$ from key area in 4 notes" in captured
|
||||
|
||||
|
||||
def test_filter_notes(test_application, mocker, capsys) -> None:
|
||||
@@ -271,10 +267,10 @@ def test_filter_notes(test_application, mocker, capsys) -> None:
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"SUCCESS +\| Loaded.*\d+.*notes from.*\d+.*total", re.DOTALL)
|
||||
assert "02 inline/inline 2.md" in captured.out
|
||||
assert "03 mixed/mixed 1.md" not in captured.out
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert captured == Regex(r"SUCCESS +\| Loaded \d+ notes from \d+ total", re.DOTALL)
|
||||
assert "02 inline/inline 2.md" in captured
|
||||
assert "03 mixed/mixed 1.md" not in captured
|
||||
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
@@ -326,11 +322,11 @@ def test_filter_clear(test_application, mocker, capsys) -> None:
|
||||
)
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert "02 inline/inline 2.md" in captured.out
|
||||
assert "03 mixed/mixed 1.md" in captured.out
|
||||
assert "01 frontmatter/frontmatter 4.md" in captured.out
|
||||
assert "04 no metadata/no_metadata_1.md " in captured.out
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert "02 inline/inline 2.md" in captured
|
||||
assert "03 mixed/mixed 1.md" in captured
|
||||
assert "01 frontmatter/frontmatter 4.md" in captured
|
||||
assert "04 no metadata/no_metadata_1.md " in captured
|
||||
|
||||
|
||||
def test_inspect_metadata_all(test_application, mocker, capsys) -> None:
|
||||
@@ -348,8 +344,8 @@ def test_inspect_metadata_all(test_application, mocker, capsys) -> None:
|
||||
)
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"type +│ article", re.DOTALL)
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert captured == Regex(r"type +│ article", re.DOTALL)
|
||||
|
||||
|
||||
def test_rename_inline_tag(test_application, mocker, capsys) -> None:
|
||||
@@ -375,8 +371,8 @@ def test_rename_inline_tag(test_application, mocker, capsys) -> None:
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"WARNING +\| No notes were changed", re.DOTALL)
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert "No notes were changed" in captured
|
||||
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
@@ -397,8 +393,8 @@ def test_rename_inline_tag(test_application, mocker, capsys) -> None:
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"Renamed.*breakfast.*to.*new_tag.*in.*\d+.*notes", re.DOTALL)
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert captured == Regex(r"Renamed breakfast to new_tag in \d+ notes", re.DOTALL)
|
||||
|
||||
|
||||
def test_rename_key(test_application, mocker, capsys) -> None:
|
||||
@@ -424,8 +420,8 @@ def test_rename_key(test_application, mocker, capsys) -> None:
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert "WARNING | No notes were changed" in captured.out
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert "WARNING | No notes were changed" in captured
|
||||
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
@@ -446,8 +442,8 @@ def test_rename_key(test_application, mocker, capsys) -> None:
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"Renamed.*tags.*to.*new_tags.*in.*\d+.*notes", re.DOTALL)
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert captured == Regex(r"Renamed tags to new_tags in \d+ notes", re.DOTALL)
|
||||
|
||||
|
||||
def test_rename_value_fail(test_application, mocker, capsys) -> None:
|
||||
@@ -476,8 +472,8 @@ def test_rename_value_fail(test_application, mocker, capsys) -> None:
|
||||
)
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"WARNING +\| No notes were changed", re.DOTALL)
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert "WARNING | No notes were changed" in captured
|
||||
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
@@ -501,11 +497,10 @@ def test_rename_value_fail(test_application, mocker, capsys) -> None:
|
||||
)
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(
|
||||
r"SUCCESS +\| Renamed.*'area:frontmatter'.*to.*'area:new_key'", re.DOTALL
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert captured == Regex(
|
||||
r"SUCCESS +\| Renamed 'area:frontmatter' to 'area:new_key' in \d+ notes", re.DOTALL
|
||||
)
|
||||
assert captured.out == Regex(r".*in.*\d+.*notes.*", re.DOTALL)
|
||||
|
||||
|
||||
def test_review_no_changes(test_application, mocker, capsys) -> None:
|
||||
@@ -518,8 +513,8 @@ def test_review_no_changes(test_application, mocker, capsys) -> None:
|
||||
)
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"INFO +\| No changes to review", re.DOTALL)
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert "INFO | No changes to review" in captured
|
||||
|
||||
|
||||
def test_review_changes(test_application, mocker, capsys) -> None:
|
||||
@@ -530,10 +525,6 @@ def test_review_changes(test_application, mocker, capsys) -> None:
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["rename_metadata", "review_changes", KeyError],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_confirm",
|
||||
return_value=True,
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_existing_key",
|
||||
return_value="tags",
|
||||
@@ -548,10 +539,49 @@ def test_review_changes(test_application, mocker, capsys) -> None:
|
||||
)
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r".*Found.*\d+.*changed notes in the vault.*", re.DOTALL)
|
||||
assert "- tags:" in captured.out
|
||||
assert "+ new_tags:" in captured.out
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert captured == Regex(r".*Found \d+ changed notes in the vault", re.DOTALL)
|
||||
assert "- tags:" in captured
|
||||
assert "+ new_tags:" in captured
|
||||
|
||||
|
||||
def test_transpose_metadata(test_application, mocker, capsys) -> None:
|
||||
"""Transpose metadata."""
|
||||
app = test_application
|
||||
app._load_vault()
|
||||
|
||||
assert app.vault.metadata.inline_metadata["inline_key"] == ["inline_key_value"]
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["transpose_metadata", KeyError],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_selection",
|
||||
side_effect=["inline_to_frontmatter", "transpose_all"],
|
||||
)
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
assert app.vault.metadata.inline_metadata == {}
|
||||
assert app.vault.metadata.frontmatter["inline_key"] == ["inline_key_value"]
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert "SUCCESS | Transposed Inline Metadata to Frontmatter in 5 notes" in captured
|
||||
|
||||
app = test_application
|
||||
app._load_vault()
|
||||
|
||||
assert app.vault.metadata.frontmatter["date_created"] == ["2022-12-21", "2022-12-22"]
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["transpose_metadata", KeyError],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_selection",
|
||||
side_effect=["frontmatter_to_inline", "transpose_all"],
|
||||
)
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
assert app.vault.metadata.inline_metadata["date_created"] == ["2022-12-21", "2022-12-22"]
|
||||
assert app.vault.metadata.frontmatter == {}
|
||||
|
||||
|
||||
def test_vault_backup(test_application, mocker, capsys) -> None:
|
||||
@@ -569,8 +599,10 @@ def test_vault_backup(test_application, mocker, capsys) -> None:
|
||||
)
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"SUCCESS +\|.*application\.bak", re.DOTALL)
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert captured == Regex(
|
||||
r"SUCCESS +\| Vault backed up to:[-\w\d\/\s]+application\.bak", re.DOTALL
|
||||
)
|
||||
|
||||
|
||||
def test_vault_delete(test_application, mocker, capsys, tmp_path) -> None:
|
||||
@@ -590,5 +622,5 @@ def test_vault_delete(test_application, mocker, capsys, tmp_path) -> None:
|
||||
)
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"SUCCESS +\| Backup deleted", re.DOTALL)
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert captured == Regex(r"SUCCESS +\| Backup deleted", re.DOTALL)
|
||||
|
||||
@@ -108,9 +108,9 @@ def test_no_config_no_vault(tmp_path, mocker) -> None:
|
||||
# Folders within the vault to ignore when indexing metadata
|
||||
exclude_paths = [".git", ".obsidian"]
|
||||
|
||||
# Location to add metadata. One of:
|
||||
# Location to add new metadata. One of:
|
||||
# TOP: Directly after frontmatter.
|
||||
# AFTER_TITLE: After a header following frontmatter.
|
||||
# AFTER_TITLE: After the first header following frontmatter.
|
||||
# BOTTOM: The bottom of the note
|
||||
insert_location = "BOTTOM\"
|
||||
"""
|
||||
|
||||
@@ -38,8 +38,14 @@ def sample_note(tmp_path) -> Path:
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def short_note(tmp_path) -> Path:
|
||||
"""Fixture which creates a temporary short note file."""
|
||||
def short_notes(tmp_path) -> Path:
|
||||
"""Fixture which creates two temporary note files.
|
||||
|
||||
Yields:
|
||||
Tuple[Path, Path]: Tuple of two temporary note files.
|
||||
1. Very short note with frontmatter
|
||||
2. Very short note without any frontmatter
|
||||
"""
|
||||
source_file1: Path = Path("tests/fixtures/short_textfile.md")
|
||||
source_file2: Path = Path("tests/fixtures/no_metadata.md")
|
||||
if not source_file1.exists():
|
||||
|
||||
6
tests/fixtures/broken_frontmatter.md
vendored
Normal file
6
tests/fixtures/broken_frontmatter.md
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
tags:
|
||||
invalid = = "content"
|
||||
---
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est la
|
||||
@@ -3,25 +3,16 @@ area: frontmatter
|
||||
date_created: 2022-12-22
|
||||
date_modified: 2022-12-22
|
||||
tags:
|
||||
- food/fruit/apple
|
||||
- food/fruit/pear
|
||||
- dinner
|
||||
- lunch
|
||||
- breakfast
|
||||
thoughts:
|
||||
rating: 8
|
||||
reviewable: false
|
||||
levels:
|
||||
level1:
|
||||
- level1a
|
||||
- level1b
|
||||
level2:
|
||||
- level2a
|
||||
- level2b
|
||||
- food/fruit/apple
|
||||
- food/fruit/pear
|
||||
- dinner
|
||||
- lunch
|
||||
- breakfast
|
||||
author: John Doe
|
||||
status: new
|
||||
type: ["book", "article", "note", "one-off"]
|
||||
---
|
||||
|
||||
# Page Title H1
|
||||
|
||||
# Headings
|
||||
|
||||
@@ -3,25 +3,16 @@ area: frontmatter
|
||||
date_created: 2022-12-22
|
||||
date_modified: 2022-11-14
|
||||
tags:
|
||||
- food/fruit/apple
|
||||
- food/fruit/pear
|
||||
- dinner
|
||||
- lunch
|
||||
- breakfast
|
||||
thoughts:
|
||||
rating: 8
|
||||
reviewable: false
|
||||
levels:
|
||||
level1:
|
||||
- level1a
|
||||
- level1b
|
||||
level2:
|
||||
- level2a
|
||||
- level2b
|
||||
- food/fruit/apple
|
||||
- food/fruit/pear
|
||||
- dinner
|
||||
- lunch
|
||||
- breakfast
|
||||
author: John Doe
|
||||
status: new
|
||||
type: ["book", "article", "note"]
|
||||
---
|
||||
|
||||
# Page Title H1
|
||||
|
||||
# Headings
|
||||
|
||||
@@ -3,25 +3,16 @@ area: frontmatter
|
||||
date_created: 2022-12-22
|
||||
date_modified: 2022-10-01
|
||||
tags:
|
||||
- food/fruit/apple
|
||||
- food/fruit/pear
|
||||
- dinner
|
||||
- lunch
|
||||
- breakfast
|
||||
thoughts:
|
||||
rating: 8
|
||||
reviewable: false
|
||||
levels:
|
||||
level1:
|
||||
- level1a
|
||||
- level1b
|
||||
level2:
|
||||
- level2a
|
||||
- level2b
|
||||
- food/fruit/apple
|
||||
- food/fruit/pear
|
||||
- dinner
|
||||
- lunch
|
||||
- breakfast
|
||||
author: John Doe
|
||||
status: new
|
||||
type: ["book", "article", "note"]
|
||||
---
|
||||
|
||||
# Page Title H1
|
||||
|
||||
# Headings
|
||||
|
||||
@@ -3,21 +3,11 @@ area: frontmatter
|
||||
date_created: 2022-12-22
|
||||
date_modified: 2022-12-22
|
||||
tags:
|
||||
- food/fruit/apple
|
||||
- food/fruit/pear
|
||||
- dinner
|
||||
- lunch
|
||||
- breakfast
|
||||
thoughts:
|
||||
rating: 8
|
||||
reviewable: false
|
||||
levels:
|
||||
level1:
|
||||
- level1a
|
||||
- level1b
|
||||
level2:
|
||||
- level2a
|
||||
- level2b
|
||||
- food/fruit/apple
|
||||
- food/fruit/pear
|
||||
- dinner
|
||||
- lunch
|
||||
- breakfast
|
||||
author: John Doe
|
||||
status: new
|
||||
type: ["book", "article", "note"]
|
||||
|
||||
@@ -6,10 +6,6 @@ tags:
|
||||
- breakfast
|
||||
- not_food
|
||||
author: John Doe
|
||||
nested_list:
|
||||
nested_list_one:
|
||||
- nested_list_one_a
|
||||
- nested_list_one_b
|
||||
type:
|
||||
- article
|
||||
- note
|
||||
|
||||
18
tests/fixtures/test_vault/test1.md
vendored
18
tests/fixtures/test_vault/test1.md
vendored
@@ -1,14 +1,16 @@
|
||||
---
|
||||
date_created: 2022-12-22
|
||||
tags:
|
||||
- shared_tag
|
||||
- frontmatter_tag1
|
||||
- frontmatter_tag2
|
||||
-
|
||||
- 📅/frontmatter_tag3
|
||||
- shared_tag
|
||||
- frontmatter_tag1
|
||||
- frontmatter_tag2
|
||||
-
|
||||
- 📅/frontmatter_tag3
|
||||
frontmatter_Key1: author name
|
||||
frontmatter_Key2: ["article", "note"]
|
||||
shared_key1: shared_key1_value
|
||||
shared_key1:
|
||||
- shared_key1_value
|
||||
- shared_key1_value3
|
||||
shared_key2: shared_key2_value1
|
||||
---
|
||||
|
||||
@@ -18,10 +20,12 @@ top_key1:: top_key1_value
|
||||
**top_key2:: top_key2_value**
|
||||
top_key3:: [[top_key3_value_as_link]]
|
||||
shared_key1:: shared_key1_value
|
||||
shared_key1:: shared_key1_value2
|
||||
shared_key2:: shared_key2_value2
|
||||
emoji_📅_key:: emoji_📅_key_value
|
||||
key📅:: 📅_key_value
|
||||
|
||||
# Heading 1
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. #intext_tag1 Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu [intext_key:: intext_value] fugiat nulla (#intext_tag2) pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est lab
|
||||
|
||||
```python
|
||||
|
||||
@@ -22,6 +22,19 @@ class KeyInputs:
|
||||
THREE = "3"
|
||||
|
||||
|
||||
def remove_ansi(text) -> str:
|
||||
"""Remove ANSI escape sequences from a string.
|
||||
|
||||
Args:
|
||||
text (str): String to remove ANSI escape sequences from.
|
||||
|
||||
Returns:
|
||||
str: String without ANSI escape sequences.
|
||||
"""
|
||||
ansi_chars = re.compile(r"(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]")
|
||||
return ansi_chars.sub("", text)
|
||||
|
||||
|
||||
class Regex:
|
||||
"""Assert that a given string meets some expectations.
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ from obsidian_metadata.models.metadata import (
|
||||
InlineTags,
|
||||
VaultMetadata,
|
||||
)
|
||||
from tests.helpers import Regex
|
||||
from tests.helpers import Regex, remove_ansi
|
||||
|
||||
FILE_CONTENT: str = Path("tests/fixtures/test_vault/test1.md").read_text()
|
||||
TAG_LIST: list[str] = ["tag 1", "tag 2", "tag 3"]
|
||||
@@ -46,7 +46,6 @@ horizontal: rule
|
||||
"""
|
||||
INLINE_CONTENT = """\
|
||||
repeated_key:: repeated_key_value1
|
||||
|
||||
#inline_tag_top1,#inline_tag_top2
|
||||
**bold_key1**:: bold_key1_value
|
||||
**bold_key2:: bold_key2_value**
|
||||
@@ -89,6 +88,22 @@ def test_frontmatter_create() -> None:
|
||||
}
|
||||
|
||||
|
||||
def test_frontmatter_create_error() -> None:
|
||||
"""Test frontmatter creation error.
|
||||
|
||||
GIVEN frontmatter content
|
||||
WHEN frontmatter is invalid
|
||||
THEN raise ValueError
|
||||
"""
|
||||
fn = """---
|
||||
tags: tag
|
||||
invalid = = "content"
|
||||
---
|
||||
"""
|
||||
with pytest.raises(AttributeError):
|
||||
Frontmatter(fn)
|
||||
|
||||
|
||||
def test_frontmatter_contains() -> None:
|
||||
"""Test frontmatter contains."""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
@@ -280,9 +295,6 @@ def test_inline_metadata_add() -> None:
|
||||
"tag_key": ["tag_key_value"],
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
assert inline.add("added_key1", "added_value_2") is True
|
||||
|
||||
assert inline.dict == {
|
||||
"added_key": [],
|
||||
"added_key1": ["added_value"],
|
||||
@@ -309,6 +321,8 @@ def test_inline_metadata_add() -> None:
|
||||
"repeated_key": ["repeated_key_value1", "repeated_key_value2"],
|
||||
"tag_key": ["tag_key_value"],
|
||||
}
|
||||
assert inline.add("repeated_key", "repeated_key_value1") is False
|
||||
assert inline.add("repeated_key", "new_value") is True
|
||||
|
||||
|
||||
def test_inline_metadata_contains() -> None:
|
||||
@@ -595,39 +609,39 @@ def test_vault_metadata_print(capsys) -> None:
|
||||
vm.index_metadata(area=MetadataType.TAGS, metadata=TAG_LIST)
|
||||
|
||||
vm.print_metadata(area=MetadataType.ALL)
|
||||
captured = capsys.readouterr()
|
||||
assert "All metadata" in captured.out
|
||||
assert "All inline tags" in captured.out
|
||||
assert "┃ Keys ┃ Values ┃" in captured.out
|
||||
assert "│ shared_key1 │ shared_key1_value │" in captured.out
|
||||
assert captured.out == Regex("#tag 1 +#tag 2")
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert "All metadata" in captured
|
||||
assert "All inline tags" in captured
|
||||
assert "┃ Keys ┃ Values ┃" in captured
|
||||
assert "│ shared_key1 │ shared_key1_value │" in captured
|
||||
assert captured == Regex("#tag 1 +#tag 2")
|
||||
|
||||
vm.print_metadata(area=MetadataType.FRONTMATTER)
|
||||
captured = capsys.readouterr()
|
||||
assert "All frontmatter" in captured.out
|
||||
assert "┃ Keys ┃ Values ┃" in captured.out
|
||||
assert "│ shared_key1 │ shared_key1_value │" in captured.out
|
||||
assert "value1" not in captured.out
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert "All frontmatter" in captured
|
||||
assert "┃ Keys ┃ Values ┃" in captured
|
||||
assert "│ shared_key1 │ shared_key1_value │" in captured
|
||||
assert "value1" not in captured
|
||||
|
||||
vm.print_metadata(area=MetadataType.INLINE)
|
||||
captured = capsys.readouterr()
|
||||
assert "All inline" in captured.out
|
||||
assert "┃ Keys ┃ Values ┃" in captured.out
|
||||
assert "shared_key1" not in captured.out
|
||||
assert "│ key1 │ value1 │" in captured.out
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert "All inline" in captured
|
||||
assert "┃ Keys ┃ Values ┃" in captured
|
||||
assert "shared_key1" not in captured
|
||||
assert "│ key1 │ value1 │" in captured
|
||||
|
||||
vm.print_metadata(area=MetadataType.TAGS)
|
||||
captured = capsys.readouterr()
|
||||
assert "All inline tags " in captured.out
|
||||
assert "┃ Keys ┃ Values ┃" not in captured.out
|
||||
assert captured.out == Regex("#tag 1 +#tag 2")
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert "All inline tags " in captured
|
||||
assert "┃ Keys ┃ Values ┃" not in captured
|
||||
assert captured == Regex("#tag 1 +#tag 2")
|
||||
|
||||
vm.print_metadata(area=MetadataType.KEYS)
|
||||
captured = capsys.readouterr()
|
||||
assert "All Keys " in captured.out
|
||||
assert "┃ Keys ┃ Values ┃" not in captured.out
|
||||
assert captured.out != Regex("#tag 1 +#tag 2")
|
||||
assert captured.out == Regex("frontmatter_Key1 +frontmatter_Key2")
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert "All Keys " in captured
|
||||
assert "┃ Keys ┃ Values ┃" not in captured
|
||||
assert captured != Regex("#tag 1 +#tag 2")
|
||||
assert captured == Regex("frontmatter_Key1 +frontmatter_Key2")
|
||||
|
||||
|
||||
def test_vault_metadata_contains() -> None:
|
||||
|
||||
1182
tests/notes_test.py
1182
tests/notes_test.py
File diff suppressed because it is too large
Load Diff
@@ -29,12 +29,16 @@ def test_vault_creation(test_vault):
|
||||
"bottom_key1": ["bottom_key1_value"],
|
||||
"bottom_key2": ["bottom_key2_value"],
|
||||
"date_created": ["2022-12-22"],
|
||||
"emoji_📅_key": ["emoji_📅_key_value"],
|
||||
"frontmatter_Key1": ["author name"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"ignored_frontmatter": ["ignore_me"],
|
||||
"intext_key": ["intext_value"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"key📅": ["📅_key_value"],
|
||||
"shared_key1": [
|
||||
"shared_key1_value",
|
||||
"shared_key1_value2",
|
||||
"shared_key1_value3",
|
||||
],
|
||||
"shared_key2": ["shared_key2_value1", "shared_key2_value2"],
|
||||
"tags": [
|
||||
"frontmatter_tag1",
|
||||
@@ -63,9 +67,9 @@ def test_vault_creation(test_vault):
|
||||
assert vault.metadata.inline_metadata == {
|
||||
"bottom_key1": ["bottom_key1_value"],
|
||||
"bottom_key2": ["bottom_key2_value"],
|
||||
"emoji_📅_key": ["emoji_📅_key_value"],
|
||||
"intext_key": ["intext_value"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"key📅": ["📅_key_value"],
|
||||
"shared_key1": ["shared_key1_value", "shared_key1_value2"],
|
||||
"shared_key2": ["shared_key2_value2"],
|
||||
"top_key1": ["top_key1_value"],
|
||||
"top_key2": ["top_key2_value"],
|
||||
@@ -77,7 +81,7 @@ def test_vault_creation(test_vault):
|
||||
"frontmatter_Key1": ["author name"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"ignored_frontmatter": ["ignore_me"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"shared_key1": ["shared_key1_value", "shared_key1_value3"],
|
||||
"shared_key2": ["shared_key2_value1"],
|
||||
"tags": [
|
||||
"frontmatter_tag1",
|
||||
@@ -91,6 +95,20 @@ def test_vault_creation(test_vault):
|
||||
}
|
||||
|
||||
|
||||
def set_insert_location(test_vault):
|
||||
"""Test setting a new insert location."""
|
||||
vault_path = test_vault
|
||||
config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path)
|
||||
vault_config = config.vaults[0]
|
||||
vault = Vault(config=vault_config)
|
||||
|
||||
assert vault.name == "vault"
|
||||
assert vault.vault_path == vault_path
|
||||
assert vault.insert_location == InsertLocation.BOTTOM
|
||||
vault.insert_location = InsertLocation.TOP
|
||||
assert vault.insert_location == InsertLocation.TOP
|
||||
|
||||
|
||||
def test_add_metadata(test_vault) -> None:
|
||||
"""Test adding metadata to the vault."""
|
||||
vault_path = test_vault
|
||||
@@ -104,13 +122,17 @@ def test_add_metadata(test_vault) -> None:
|
||||
"bottom_key1": ["bottom_key1_value"],
|
||||
"bottom_key2": ["bottom_key2_value"],
|
||||
"date_created": ["2022-12-22"],
|
||||
"emoji_📅_key": ["emoji_📅_key_value"],
|
||||
"frontmatter_Key1": ["author name"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"ignored_frontmatter": ["ignore_me"],
|
||||
"intext_key": ["intext_value"],
|
||||
"key📅": ["📅_key_value"],
|
||||
"new_key": [],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"shared_key1": [
|
||||
"shared_key1_value",
|
||||
"shared_key1_value2",
|
||||
"shared_key1_value3",
|
||||
],
|
||||
"shared_key2": ["shared_key2_value1", "shared_key2_value2"],
|
||||
"tags": [
|
||||
"frontmatter_tag1",
|
||||
@@ -132,7 +154,7 @@ def test_add_metadata(test_vault) -> None:
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"ignored_frontmatter": ["ignore_me"],
|
||||
"new_key": [],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"shared_key1": ["shared_key1_value", "shared_key1_value3"],
|
||||
"shared_key2": ["shared_key2_value1"],
|
||||
"tags": [
|
||||
"frontmatter_tag1",
|
||||
@@ -150,14 +172,18 @@ def test_add_metadata(test_vault) -> None:
|
||||
"bottom_key1": ["bottom_key1_value"],
|
||||
"bottom_key2": ["bottom_key2_value"],
|
||||
"date_created": ["2022-12-22"],
|
||||
"emoji_📅_key": ["emoji_📅_key_value"],
|
||||
"frontmatter_Key1": ["author name"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"ignored_frontmatter": ["ignore_me"],
|
||||
"intext_key": ["intext_value"],
|
||||
"key📅": ["📅_key_value"],
|
||||
"new_key": [],
|
||||
"new_key2": ["new_key2_value"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"shared_key1": [
|
||||
"shared_key1_value",
|
||||
"shared_key1_value2",
|
||||
"shared_key1_value3",
|
||||
],
|
||||
"shared_key2": ["shared_key2_value1", "shared_key2_value2"],
|
||||
"tags": [
|
||||
"frontmatter_tag1",
|
||||
@@ -180,7 +206,7 @@ def test_add_metadata(test_vault) -> None:
|
||||
"ignored_frontmatter": ["ignore_me"],
|
||||
"new_key": [],
|
||||
"new_key2": ["new_key2_value"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"shared_key1": ["shared_key1_value", "shared_key1_value3"],
|
||||
"shared_key2": ["shared_key2_value1"],
|
||||
"tags": [
|
||||
"frontmatter_tag1",
|
||||
@@ -336,7 +362,7 @@ def test_export_csv(tmp_path, test_vault):
|
||||
vault = Vault(config=vault_config)
|
||||
export_file = Path(f"{tmp_path}/export.csv")
|
||||
|
||||
vault.export_metadata(path=export_file, format="csv")
|
||||
vault.export_metadata(path=export_file, export_format="csv")
|
||||
assert export_file.exists() is True
|
||||
assert "frontmatter,date_created,2022-12-22" in export_file.read_text()
|
||||
|
||||
@@ -349,7 +375,7 @@ def test_export_json(tmp_path, test_vault):
|
||||
vault = Vault(config=vault_config)
|
||||
export_file = Path(f"{tmp_path}/export.json")
|
||||
|
||||
vault.export_metadata(path=export_file, format="json")
|
||||
vault.export_metadata(path=export_file, export_format="json")
|
||||
assert export_file.exists() is True
|
||||
assert '"frontmatter": {' in export_file.read_text()
|
||||
|
||||
@@ -470,3 +496,51 @@ def test_rename_metadata(test_vault) -> None:
|
||||
"shared_tag",
|
||||
"📅/frontmatter_tag3",
|
||||
]
|
||||
|
||||
|
||||
def test_transpose_metadata(test_vault) -> None:
|
||||
"""Test transposing metadata."""
|
||||
vault_path = test_vault
|
||||
config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path)
|
||||
vault_config = config.vaults[0]
|
||||
vault = Vault(config=vault_config)
|
||||
|
||||
assert vault.transpose_metadata(begin=MetadataType.INLINE, end=MetadataType.FRONTMATTER) == 2
|
||||
|
||||
assert vault.metadata.inline_metadata == {}
|
||||
assert vault.metadata.frontmatter == {
|
||||
"author": ["author name"],
|
||||
"bottom_key1": ["bottom_key1_value"],
|
||||
"bottom_key2": ["bottom_key2_value"],
|
||||
"date_created": ["2022-12-22"],
|
||||
"frontmatter_Key1": ["author name"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"ignored_frontmatter": ["ignore_me"],
|
||||
"intext_key": ["intext_value"],
|
||||
"key📅": ["📅_key_value"],
|
||||
"shared_key1": [
|
||||
"shared_key1_value",
|
||||
"shared_key1_value2",
|
||||
"shared_key1_value3",
|
||||
],
|
||||
"shared_key2": ["shared_key2_value1", "shared_key2_value2"],
|
||||
"tags": [
|
||||
"frontmatter_tag1",
|
||||
"frontmatter_tag2",
|
||||
"frontmatter_tag3",
|
||||
"ignored_file_tag1",
|
||||
"shared_tag",
|
||||
"📅/frontmatter_tag3",
|
||||
],
|
||||
"top_key1": ["top_key1_value"],
|
||||
"top_key2": ["top_key2_value"],
|
||||
"top_key3": ["top_key3_value_as_link"],
|
||||
"type": ["article", "note"],
|
||||
}
|
||||
|
||||
assert (
|
||||
vault.transpose_metadata(
|
||||
begin=MetadataType.INLINE, end=MetadataType.FRONTMATTER, location=InsertLocation.TOP
|
||||
)
|
||||
== 0
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user