atlas-vault 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- atlas_vault-0.1.0/LICENSE +21 -0
- atlas_vault-0.1.0/PKG-INFO +183 -0
- atlas_vault-0.1.0/README.md +165 -0
- atlas_vault-0.1.0/atlas/__init__.py +18 -0
- atlas_vault-0.1.0/atlas/__main__.py +10 -0
- atlas_vault-0.1.0/atlas/commands/__init__.py +21 -0
- atlas_vault-0.1.0/atlas/commands/_types.py +82 -0
- atlas_vault-0.1.0/atlas/commands/create_cmd.py +187 -0
- atlas_vault-0.1.0/atlas/commands/delete_cmd.py +55 -0
- atlas_vault-0.1.0/atlas/commands/init_cmd.py +42 -0
- atlas_vault-0.1.0/atlas/commands/list_cmd.py +137 -0
- atlas_vault-0.1.0/atlas/commands/move_cmd.py +148 -0
- atlas_vault-0.1.0/atlas/commands/search_cmd.py +86 -0
- atlas_vault-0.1.0/atlas/errors.py +291 -0
- atlas_vault-0.1.0/atlas/frontmatter.py +143 -0
- atlas_vault-0.1.0/atlas/main.py +52 -0
- atlas_vault-0.1.0/atlas/note.py +98 -0
- atlas_vault-0.1.0/atlas/templates.py +77 -0
- atlas_vault-0.1.0/atlas/vault.py +706 -0
- atlas_vault-0.1.0/atlas/wikilinks.py +73 -0
- atlas_vault-0.1.0/atlas_vault.egg-info/PKG-INFO +183 -0
- atlas_vault-0.1.0/atlas_vault.egg-info/SOURCES.txt +26 -0
- atlas_vault-0.1.0/atlas_vault.egg-info/dependency_links.txt +1 -0
- atlas_vault-0.1.0/atlas_vault.egg-info/entry_points.txt +2 -0
- atlas_vault-0.1.0/atlas_vault.egg-info/requires.txt +1 -0
- atlas_vault-0.1.0/atlas_vault.egg-info/top_level.txt +1 -0
- atlas_vault-0.1.0/pyproject.toml +32 -0
- atlas_vault-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ryan Mattson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: atlas-vault
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A command-line tool for managing and validating Markdown-based worldbuilding vaults.
|
|
5
|
+
Author-email: Ryan Mattson <ramattson5@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/you/atlas
|
|
8
|
+
Project-URL: Issues, https://github.com/you/atlas/issues
|
|
9
|
+
Keywords: worldbuilding,markdown,cli,vault
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Topic :: Text Processing :: Markup :: Markdown
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: pyyaml>=6.0
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# Atlas
|
|
20
|
+
|
|
21
|
+
A command-line tool for managing Markdown worldbuilding vaults, built with Obsidian in mind.
|
|
22
|
+
|
|
23
|
+
Atlas creates, moves, deletes, lists, and searches notes in a vault of plain Markdown files. It keeps note ids and `[[wiki links]]` consistent when you reorganize, so renaming a note never leaves broken links behind.
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
- Create notes from templates, with frontmatter written for you
|
|
28
|
+
- Move or rename notes and automatically update every wiki link that points at them
|
|
29
|
+
- Safe deletes: notes go to a trash folder instead of being erased
|
|
30
|
+
- Tree view of your vault and title search
|
|
31
|
+
- Works on plain `.md` files, so your vault stays usable in Obsidian or any other editor
|
|
32
|
+
|
|
33
|
+
## Requirements
|
|
34
|
+
|
|
35
|
+
- Python 3.11 or newer
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
cd ~/Documents/MyWorld
|
|
45
|
+
atlas init
|
|
46
|
+
atlas create "Nations/Republic of Valora" --type Nation
|
|
47
|
+
atlas list
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`atlas init` marks a folder as an Atlas vault by creating a `.atlas` directory inside it. Every other command needs that directory to exist.
|
|
51
|
+
|
|
52
|
+
## Finding your vault
|
|
53
|
+
|
|
54
|
+
Atlas looks for a vault in this order:
|
|
55
|
+
|
|
56
|
+
1. The path given with `-v` / `--vault`
|
|
57
|
+
2. The current directory, then up to five parent directories, stopping at the first one that contains `.atlas`
|
|
58
|
+
|
|
59
|
+
So you can run commands from any subfolder of your vault without passing a path. Global options go before the command:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
atlas --vault ~/Documents/MyWorld list
|
|
63
|
+
atlas --quiet create "Characters/Aria"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
| Option | Description |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| `-v`, `--vault PATH` | Path to the vault. |
|
|
69
|
+
| `--quiet` | Silence messages from Atlas. |
|
|
70
|
+
| `--version` | Show the Atlas version. |
|
|
71
|
+
|
|
72
|
+
## Commands
|
|
73
|
+
|
|
74
|
+
Note names are paths relative to the vault root. The `.md` extension is optional.
|
|
75
|
+
|
|
76
|
+
### `atlas init`
|
|
77
|
+
|
|
78
|
+
Initializes the current directory (or `--vault`) as an Atlas vault. This creates `.atlas/` directory.
|
|
79
|
+
|
|
80
|
+
### `atlas create NAME [NAME ...]`
|
|
81
|
+
|
|
82
|
+
Creates one or more notes. Missing folders are created for you. Each new note gets an `id` in its frontmatter, generated from its path, and a heading with its title.
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
atlas create "Nations/Republic of Valora"
|
|
86
|
+
atlas create "Characters/Aria" "Characters/Bram" --type character
|
|
87
|
+
atlas create "Places/Old Harbor" --template location --open
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
| Option | Description |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| `--type TYPE` | Sets `type` in the note's frontmatter, lowercased. |
|
|
93
|
+
| `-t`, `--template NAME` | Starts the note from a template in `.atlas/templates`. |
|
|
94
|
+
| `--open` | Opens the last created note in `$EDITOR`. |
|
|
95
|
+
|
|
96
|
+
### `atlas list`
|
|
97
|
+
|
|
98
|
+
Prints a tree of every Markdown file in the vault. Hidden files and folders are skipped unless you pass `-a` / `--all`.
|
|
99
|
+
|
|
100
|
+
### `atlas search QUERY`
|
|
101
|
+
|
|
102
|
+
Prints the vault-relative path of every note whose title contains the query. The match is case-insensitive and only looks at titles, not folders or note contents. Use `-a` / `--all` to include hidden directories such as the trash.
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
atlas search valora
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### `atlas move SOURCE [SOURCE ...] DESTINATION`
|
|
109
|
+
|
|
110
|
+
Moves or renames notes.
|
|
111
|
+
|
|
112
|
+
- With one source, the destination is the new path of the note.
|
|
113
|
+
- With several sources, the destination must be an existing folder.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
atlas move "Nations/Valora" "Nations/Republic of Valora"
|
|
117
|
+
atlas move "Characters/Aria" "Characters/Bram" Archive
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
When a note moves, Atlas:
|
|
121
|
+
|
|
122
|
+
- Rewrites the note's frontmatter `id` to match its new path
|
|
123
|
+
- Updates wiki links in every note in the vault, including aliases (`[[Note|alias]]`), headings (`[[Note#Heading]]`), and embeds (`![[Note]]`)
|
|
124
|
+
- Leaves links inside code blocks and inline code untouched
|
|
125
|
+
- Leaves a bare-title link alone when several notes share that title, since it is unclear which one was meant
|
|
126
|
+
|
|
127
|
+
All moves are checked before anything changes, so a bad argument never leaves the vault half moved. Pass `--no-links` to skip the link updates.
|
|
128
|
+
|
|
129
|
+
### `atlas delete NAME [NAME ...]`
|
|
130
|
+
|
|
131
|
+
Moves notes to the vault's `.trash` folder instead of erasing them. A timestamp is added to the file name, so deleting a note with the same name twice never overwrites the earlier copy.
|
|
132
|
+
|
|
133
|
+
## Templates
|
|
134
|
+
|
|
135
|
+
Templates are Markdown files in `.atlas/templates`. Pass the name without the `.md` suffix:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
atlas create "Places/Old Harbor" --template location
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Templates can have frontmatter and a body, and `{{placeholders}}` are filled in when the note is created:
|
|
142
|
+
|
|
143
|
+
| Placeholder | Value |
|
|
144
|
+
| --- | --- |
|
|
145
|
+
| `{{title}}` | The note title (its file name without `.md`). |
|
|
146
|
+
| `{{id}}` | The generated note id. |
|
|
147
|
+
| `{{date}}` | Today's date as `YYYY-MM-DD`. |
|
|
148
|
+
|
|
149
|
+
Unknown placeholders are left as they are. Example `.atlas/templates/location.md`:
|
|
150
|
+
|
|
151
|
+
```markdown
|
|
152
|
+
---
|
|
153
|
+
type: location
|
|
154
|
+
created: "{{date}}"
|
|
155
|
+
tags: []
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
# {{title}}
|
|
159
|
+
|
|
160
|
+
## Description
|
|
161
|
+
|
|
162
|
+
## Notable people
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The note `id` is always generated by Atlas, and a `--type` given on the command line overrides a `type` set in the template.
|
|
166
|
+
|
|
167
|
+
## Vault layout
|
|
168
|
+
|
|
169
|
+
```text
|
|
170
|
+
MyWorld/
|
|
171
|
+
├── .atlas/
|
|
172
|
+
│ ├── config.toml
|
|
173
|
+
│ └── templates/
|
|
174
|
+
├── .trash/
|
|
175
|
+
├── Nations/
|
|
176
|
+
│ └── Republic of Valora.md
|
|
177
|
+
└── Characters/
|
|
178
|
+
└── Aria.md
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
MIT
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Atlas
|
|
2
|
+
|
|
3
|
+
A command-line tool for managing Markdown worldbuilding vaults, built with Obsidian in mind.
|
|
4
|
+
|
|
5
|
+
Atlas creates, moves, deletes, lists, and searches notes in a vault of plain Markdown files. It keeps note ids and `[[wiki links]]` consistent when you reorganize, so renaming a note never leaves broken links behind.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Create notes from templates, with frontmatter written for you
|
|
10
|
+
- Move or rename notes and automatically update every wiki link that points at them
|
|
11
|
+
- Safe deletes: notes go to a trash folder instead of being erased
|
|
12
|
+
- Tree view of your vault and title search
|
|
13
|
+
- Works on plain `.md` files, so your vault stays usable in Obsidian or any other editor
|
|
14
|
+
|
|
15
|
+
## Requirements
|
|
16
|
+
|
|
17
|
+
- Python 3.11 or newer
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
cd ~/Documents/MyWorld
|
|
27
|
+
atlas init
|
|
28
|
+
atlas create "Nations/Republic of Valora" --type Nation
|
|
29
|
+
atlas list
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`atlas init` marks a folder as an Atlas vault by creating a `.atlas` directory inside it. Every other command needs that directory to exist.
|
|
33
|
+
|
|
34
|
+
## Finding your vault
|
|
35
|
+
|
|
36
|
+
Atlas looks for a vault in this order:
|
|
37
|
+
|
|
38
|
+
1. The path given with `-v` / `--vault`
|
|
39
|
+
2. The current directory, then up to five parent directories, stopping at the first one that contains `.atlas`
|
|
40
|
+
|
|
41
|
+
So you can run commands from any subfolder of your vault without passing a path. Global options go before the command:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
atlas --vault ~/Documents/MyWorld list
|
|
45
|
+
atlas --quiet create "Characters/Aria"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
| Option | Description |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| `-v`, `--vault PATH` | Path to the vault. |
|
|
51
|
+
| `--quiet` | Silence messages from Atlas. |
|
|
52
|
+
| `--version` | Show the Atlas version. |
|
|
53
|
+
|
|
54
|
+
## Commands
|
|
55
|
+
|
|
56
|
+
Note names are paths relative to the vault root. The `.md` extension is optional.
|
|
57
|
+
|
|
58
|
+
### `atlas init`
|
|
59
|
+
|
|
60
|
+
Initializes the current directory (or `--vault`) as an Atlas vault. This creates `.atlas/` directory.
|
|
61
|
+
|
|
62
|
+
### `atlas create NAME [NAME ...]`
|
|
63
|
+
|
|
64
|
+
Creates one or more notes. Missing folders are created for you. Each new note gets an `id` in its frontmatter, generated from its path, and a heading with its title.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
atlas create "Nations/Republic of Valora"
|
|
68
|
+
atlas create "Characters/Aria" "Characters/Bram" --type character
|
|
69
|
+
atlas create "Places/Old Harbor" --template location --open
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
| Option | Description |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| `--type TYPE` | Sets `type` in the note's frontmatter, lowercased. |
|
|
75
|
+
| `-t`, `--template NAME` | Starts the note from a template in `.atlas/templates`. |
|
|
76
|
+
| `--open` | Opens the last created note in `$EDITOR`. |
|
|
77
|
+
|
|
78
|
+
### `atlas list`
|
|
79
|
+
|
|
80
|
+
Prints a tree of every Markdown file in the vault. Hidden files and folders are skipped unless you pass `-a` / `--all`.
|
|
81
|
+
|
|
82
|
+
### `atlas search QUERY`
|
|
83
|
+
|
|
84
|
+
Prints the vault-relative path of every note whose title contains the query. The match is case-insensitive and only looks at titles, not folders or note contents. Use `-a` / `--all` to include hidden directories such as the trash.
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
atlas search valora
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### `atlas move SOURCE [SOURCE ...] DESTINATION`
|
|
91
|
+
|
|
92
|
+
Moves or renames notes.
|
|
93
|
+
|
|
94
|
+
- With one source, the destination is the new path of the note.
|
|
95
|
+
- With several sources, the destination must be an existing folder.
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
atlas move "Nations/Valora" "Nations/Republic of Valora"
|
|
99
|
+
atlas move "Characters/Aria" "Characters/Bram" Archive
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
When a note moves, Atlas:
|
|
103
|
+
|
|
104
|
+
- Rewrites the note's frontmatter `id` to match its new path
|
|
105
|
+
- Updates wiki links in every note in the vault, including aliases (`[[Note|alias]]`), headings (`[[Note#Heading]]`), and embeds (`![[Note]]`)
|
|
106
|
+
- Leaves links inside code blocks and inline code untouched
|
|
107
|
+
- Leaves a bare-title link alone when several notes share that title, since it is unclear which one was meant
|
|
108
|
+
|
|
109
|
+
All moves are checked before anything changes, so a bad argument never leaves the vault half moved. Pass `--no-links` to skip the link updates.
|
|
110
|
+
|
|
111
|
+
### `atlas delete NAME [NAME ...]`
|
|
112
|
+
|
|
113
|
+
Moves notes to the vault's `.trash` folder instead of erasing them. A timestamp is added to the file name, so deleting a note with the same name twice never overwrites the earlier copy.
|
|
114
|
+
|
|
115
|
+
## Templates
|
|
116
|
+
|
|
117
|
+
Templates are Markdown files in `.atlas/templates`. Pass the name without the `.md` suffix:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
atlas create "Places/Old Harbor" --template location
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Templates can have frontmatter and a body, and `{{placeholders}}` are filled in when the note is created:
|
|
124
|
+
|
|
125
|
+
| Placeholder | Value |
|
|
126
|
+
| --- | --- |
|
|
127
|
+
| `{{title}}` | The note title (its file name without `.md`). |
|
|
128
|
+
| `{{id}}` | The generated note id. |
|
|
129
|
+
| `{{date}}` | Today's date as `YYYY-MM-DD`. |
|
|
130
|
+
|
|
131
|
+
Unknown placeholders are left as they are. Example `.atlas/templates/location.md`:
|
|
132
|
+
|
|
133
|
+
```markdown
|
|
134
|
+
---
|
|
135
|
+
type: location
|
|
136
|
+
created: "{{date}}"
|
|
137
|
+
tags: []
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
# {{title}}
|
|
141
|
+
|
|
142
|
+
## Description
|
|
143
|
+
|
|
144
|
+
## Notable people
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The note `id` is always generated by Atlas, and a `--type` given on the command line overrides a `type` set in the template.
|
|
148
|
+
|
|
149
|
+
## Vault layout
|
|
150
|
+
|
|
151
|
+
```text
|
|
152
|
+
MyWorld/
|
|
153
|
+
├── .atlas/
|
|
154
|
+
│ ├── config.toml
|
|
155
|
+
│ └── templates/
|
|
156
|
+
├── .trash/
|
|
157
|
+
├── Nations/
|
|
158
|
+
│ └── Republic of Valora.md
|
|
159
|
+
└── Characters/
|
|
160
|
+
└── Aria.md
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
MIT
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# atlas/__init__.py
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Atlas: a command-line tool for managing and validating worldbuilding vaults.
|
|
5
|
+
|
|
6
|
+
Attributes:
|
|
7
|
+
__version__: The installed version of Atlas, read from package metadata.
|
|
8
|
+
Falls back to a development version when Atlas is not installed.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
__version__: str = version("atlas-vault")
|
|
15
|
+
except PackageNotFoundError:
|
|
16
|
+
__version__: str = "0.1.0-dev"
|
|
17
|
+
|
|
18
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# atlas/commands/__init__.py
|
|
2
|
+
|
|
3
|
+
"""Atlas subcommands."""
|
|
4
|
+
|
|
5
|
+
from atlas.commands import create_cmd, delete_cmd, init_cmd, list_cmd, move_cmd, search_cmd
|
|
6
|
+
from atlas.commands._types import SubParsers
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def register_all(subparsers: SubParsers) -> None:
|
|
10
|
+
"""
|
|
11
|
+
Registers every Atlas subcommand.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
subparsers: The subparsers action to add the commands to.
|
|
15
|
+
"""
|
|
16
|
+
init_cmd.register(subparsers)
|
|
17
|
+
create_cmd.register(subparsers)
|
|
18
|
+
list_cmd.register(subparsers)
|
|
19
|
+
delete_cmd.register(subparsers)
|
|
20
|
+
move_cmd.register(subparsers)
|
|
21
|
+
search_cmd.register(subparsers)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# atlas/commands/_types.py
|
|
2
|
+
|
|
3
|
+
"""Shared typing helpers for Atlas subcommands."""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from typing import Any, Protocol
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SubParsers(Protocol):
|
|
12
|
+
"""
|
|
13
|
+
The part of an argparse subparsers action that commands rely on.
|
|
14
|
+
|
|
15
|
+
This mirrors ``argparse._SubParsersAction`` without importing the private
|
|
16
|
+
class. Anything with a compatible ``add_parser`` method satisfies it.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def add_parser(
|
|
20
|
+
self,
|
|
21
|
+
name: str,
|
|
22
|
+
*,
|
|
23
|
+
help: str | None = None, # noqa: A002
|
|
24
|
+
) -> argparse.ArgumentParser:
|
|
25
|
+
"""Adds a named subparser and returns it."""
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def non_empty(value: str) -> str:
|
|
30
|
+
"""
|
|
31
|
+
Strips a command-line value and rejects it if nothing is left.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
value: The raw value from the command line.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
The stripped value.
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
argparse.ArgumentTypeError: If the value is empty or only whitespace.
|
|
41
|
+
"""
|
|
42
|
+
stripped: str = value.strip()
|
|
43
|
+
if not stripped:
|
|
44
|
+
raise argparse.ArgumentTypeError("must not be empty")
|
|
45
|
+
return stripped
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def key_value(value: str) -> tuple[str, Any]:
|
|
49
|
+
"""
|
|
50
|
+
Parses a ``KEY=VALUE`` command-line argument.
|
|
51
|
+
|
|
52
|
+
The text is split at the first ``=``. The key is stripped of surrounding
|
|
53
|
+
whitespace. The value is parsed as YAML, so ``2000000`` becomes an int,
|
|
54
|
+
``true`` a bool, ``[a, b]`` a list, and ``draft`` stays a string. Quote
|
|
55
|
+
the value inside the argument to force a string, as in ``name="'123'"``.
|
|
56
|
+
An empty value becomes ``None``.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
value: The raw ``KEY=VALUE`` argument.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
The key and the parsed value.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
argparse.ArgumentTypeError: If there is no ``=``, the key is empty
|
|
66
|
+
or is ``id``, or the value is not valid YAML.
|
|
67
|
+
"""
|
|
68
|
+
key, separator, raw_value = value.partition("=")
|
|
69
|
+
key = key.strip()
|
|
70
|
+
if not separator or not key:
|
|
71
|
+
raise argparse.ArgumentTypeError(f"expected KEY=VALUE, got {value!r}")
|
|
72
|
+
if key == "id":
|
|
73
|
+
raise argparse.ArgumentTypeError("'id' is set from the note path and cannot be changed")
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
parsed: Any = yaml.safe_load(raw_value)
|
|
77
|
+
except yaml.YAMLError as error:
|
|
78
|
+
problem: str = getattr(error, "problem", None) or "invalid YAML"
|
|
79
|
+
raise argparse.ArgumentTypeError(
|
|
80
|
+
f"value for {key!r} is not valid YAML ({problem}); quote it to use it as text"
|
|
81
|
+
) from error
|
|
82
|
+
return key, parsed
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# atlas/commands/create_cmd.py
|
|
2
|
+
|
|
3
|
+
"""The ``atlas create`` command."""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
import shlex
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
from atlas import frontmatter
|
|
14
|
+
from atlas.commands._types import SubParsers, non_empty
|
|
15
|
+
from atlas.errors import FileReadError, InvalidNoteIdError
|
|
16
|
+
from atlas.vault import Vault
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from atlas.note import Note
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def register(subparsers: SubParsers) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Registers the ``create`` subcommand.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
subparsers: The subparsers action to add the command to.
|
|
28
|
+
"""
|
|
29
|
+
parser: argparse.ArgumentParser = subparsers.add_parser(
|
|
30
|
+
"create", help="Create a new Atlas Object."
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"names",
|
|
35
|
+
nargs="+",
|
|
36
|
+
type=str,
|
|
37
|
+
help=(
|
|
38
|
+
"Path(s) of the new item(s) relative to the vault root, e.g. "
|
|
39
|
+
"'Nations/Republic of Valora'. The '.md' extension is optional."
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"--type",
|
|
45
|
+
type=non_empty,
|
|
46
|
+
metavar="TYPE",
|
|
47
|
+
help=(
|
|
48
|
+
"Set the type of the new note, e.g. 'Nation'. Written to its frontmatter. "
|
|
49
|
+
"Shorthand for --set type=<lowercased TYPE>."
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--set",
|
|
55
|
+
dest="properties",
|
|
56
|
+
action="append",
|
|
57
|
+
metavar="KEY=VALUE",
|
|
58
|
+
help=(
|
|
59
|
+
"Set a frontmatter property, e.g. --set status=draft. The value is parsed "
|
|
60
|
+
"as YAML, so numbers, booleans and lists work: --set population=2000000 "
|
|
61
|
+
"--set 'tags=[nation, active]'. Repeatable. Overrides --type and the template."
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--id",
|
|
67
|
+
type=non_empty,
|
|
68
|
+
metavar="ID",
|
|
69
|
+
dest="note_id",
|
|
70
|
+
help=(
|
|
71
|
+
"Use this ID instead of the one derived from the note's path. "
|
|
72
|
+
"Only valid when creating a single note."
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
parser.add_argument(
|
|
77
|
+
"-b",
|
|
78
|
+
"--body",
|
|
79
|
+
metavar="TEXT",
|
|
80
|
+
help=(
|
|
81
|
+
"Seed the note body with this text instead of the default heading or the "
|
|
82
|
+
"template body. Use '-' to read the body from standard input. When creating "
|
|
83
|
+
"several notes, every note gets the same body."
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
parser.add_argument(
|
|
88
|
+
"-t",
|
|
89
|
+
"--template",
|
|
90
|
+
type=non_empty,
|
|
91
|
+
metavar="NAME",
|
|
92
|
+
help="Name of a template in .atlas/templates to start the note from.",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
parser.add_argument(
|
|
96
|
+
"--open",
|
|
97
|
+
action="store_true",
|
|
98
|
+
help="Open the note (or last note) in the users $EDITOR. Does not open if no $EDITOR.",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
parser.set_defaults(handler=run)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _read_stdin() -> str:
|
|
105
|
+
"""
|
|
106
|
+
Reads all of standard input as text.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
The text that was piped in.
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
FileReadError: If standard input cannot be read or is not valid text.
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
return sys.stdin.read()
|
|
116
|
+
except (OSError, UnicodeDecodeError) as error:
|
|
117
|
+
reason: str = getattr(error, "strerror", None) or str(error)
|
|
118
|
+
raise FileReadError(Path("<stdin>"), reason) from error
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def run(args: argparse.Namespace) -> int:
|
|
122
|
+
"""
|
|
123
|
+
Runs the ``create`` command.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
args: The parsed command-line arguments.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
The process exit code. Zero indicates success.
|
|
130
|
+
|
|
131
|
+
Raises:
|
|
132
|
+
VaultNotFoundError: If the vault path does not exist.
|
|
133
|
+
VaultNotInitializedError: If the vault has no ``.atlas`` directory.
|
|
134
|
+
InvalidPropertyError: If a ``--set`` property is not valid.
|
|
135
|
+
InvalidNoteNameError: If the note name is not valid.
|
|
136
|
+
InvalidTemplateNameError: If the template name is not valid.
|
|
137
|
+
TemplateNotFoundError: If the template does not exist.
|
|
138
|
+
FileReadError: If the template cannot be read.
|
|
139
|
+
InvalidFrontmatterError: If the template's frontmatter is not valid.
|
|
140
|
+
NoteExistsError: If the note already exists.
|
|
141
|
+
FileCreationError: If the note or its parent folders cannot be created.
|
|
142
|
+
"""
|
|
143
|
+
vault: Vault = Vault.open(args.vault)
|
|
144
|
+
|
|
145
|
+
if args.note_id is not None and len(args.names) > 1:
|
|
146
|
+
raise InvalidNoteIdError(args.note_id, "--id can only be used when creating one note")
|
|
147
|
+
|
|
148
|
+
properties: dict[str, Any] = {}
|
|
149
|
+
if args.type is not None:
|
|
150
|
+
properties["type"] = args.type.lower()
|
|
151
|
+
|
|
152
|
+
raw_properties: list[str] = args.properties or []
|
|
153
|
+
for assignment in raw_properties:
|
|
154
|
+
key, value = frontmatter.parse_property(assignment)
|
|
155
|
+
properties[key] = value
|
|
156
|
+
|
|
157
|
+
body: str | None = args.body
|
|
158
|
+
if body == "-":
|
|
159
|
+
body = _read_stdin()
|
|
160
|
+
|
|
161
|
+
last_note: Note | None = None
|
|
162
|
+
|
|
163
|
+
for name in args.names:
|
|
164
|
+
new_note: Note = vault.resolve_note(name)
|
|
165
|
+
vault.create(
|
|
166
|
+
new_note,
|
|
167
|
+
properties=properties,
|
|
168
|
+
template=args.template,
|
|
169
|
+
note_id=args.note_id,
|
|
170
|
+
body=body,
|
|
171
|
+
)
|
|
172
|
+
last_note = new_note
|
|
173
|
+
|
|
174
|
+
if not args.quiet:
|
|
175
|
+
print("Created Note:", new_note.path)
|
|
176
|
+
|
|
177
|
+
if args.open and last_note is not None:
|
|
178
|
+
editor: str | None = os.environ.get("EDITOR", None)
|
|
179
|
+
if editor:
|
|
180
|
+
try:
|
|
181
|
+
cmd: list[str] = [*shlex.split(editor), str(last_note.path)]
|
|
182
|
+
subprocess.call(cmd) # noqa: S603
|
|
183
|
+
except FileNotFoundError:
|
|
184
|
+
print(f"Editor '{editor}' not found.")
|
|
185
|
+
return 0
|
|
186
|
+
|
|
187
|
+
return 0
|