pwdnote 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.
@@ -0,0 +1,33 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ permissions:
9
+ contents: read
10
+ id-token: write
11
+
12
+ jobs:
13
+ publish:
14
+ runs-on: ubuntu-latest
15
+ environment: pypi
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - name: Install uv
21
+ uses: astral-sh/setup-uv@v5
22
+
23
+ - name: Set up Python
24
+ run: uv python install 3.12
25
+
26
+ - name: Run tests
27
+ run: uv run pytest
28
+
29
+ - name: Build package
30
+ run: uv build
31
+
32
+ - name: Publish to PyPI
33
+ run: uv publish
@@ -0,0 +1,24 @@
1
+ # pwdnote runtime artifacts (never commit decrypted/temporary notes)
2
+ .pwdnote.tmp
3
+ .pwdnote.cache
4
+
5
+ # Python
6
+ __pycache__/
7
+ *.py[cod]
8
+ *.egg-info/
9
+ .eggs/
10
+ build/
11
+ dist/
12
+ .venv/
13
+ venv/
14
+
15
+ # Tooling
16
+ .pytest_cache/
17
+ .coverage
18
+ .coverage.*
19
+ htmlcov/
20
+ .ruff_cache/
21
+ .mypy_cache/
22
+
23
+ # OS
24
+ .DS_Store
pwdnote-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Avi Bobrovsky
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.
pwdnote-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,169 @@
1
+ Metadata-Version: 2.4
2
+ Name: pwdnote
3
+ Version: 0.1.0
4
+ Summary: Encrypted, project-local notes for your terminal.
5
+ Project-URL: Homepage, https://github.com/pwdnote/pwdnote
6
+ Project-URL: Repository, https://github.com/pwdnote/pwdnote
7
+ Project-URL: Issues, https://github.com/pwdnote/pwdnote/issues
8
+ Author: pwdnote maintainers
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: cli,encryption,notes,project,terminal
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Utilities
19
+ Requires-Python: >=3.12
20
+ Requires-Dist: cryptography>=42.0
21
+ Requires-Dist: rich>=13.7
22
+ Requires-Dist: typer>=0.12
23
+ Description-Content-Type: text/markdown
24
+
25
+ # pwdnote
26
+
27
+ **Encrypted, project-local notes for your terminal.**
28
+
29
+ `pwdnote` keeps project-specific notes — TODOs, deployment notes, AWS account
30
+ details, session IDs, customer context, reminders — encrypted on disk, right
31
+ next to your code, without ever exposing plaintext inside the repository.
32
+
33
+ It is **local-first**, **encrypted-by-default**, **Git-friendly**, and
34
+ **terminal-native**. The single encrypted file (`.pwdnote.enc`) is safe to
35
+ commit; without your key it is just ciphertext.
36
+
37
+ `pwdnote` is *not* a cloud service, a note-taking app, a password manager, a
38
+ database, or a sync platform. It does one small thing well.
39
+
40
+ ---
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ uv tool install pwdnote
46
+ ```
47
+
48
+ That's it — no further setup. The encryption key is generated automatically on
49
+ first use.
50
+
51
+ ---
52
+
53
+ ## Quick start
54
+
55
+ ```bash
56
+ cd my-project
57
+ pwdnote init # create .pwdnote.enc
58
+ pwdnote edit # open it in your editor
59
+ pwdnote # print the decrypted note
60
+ pwdnote add "Remember to rotate AWS credentials"
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Commands
66
+
67
+ | Command | Description |
68
+ | --- | --- |
69
+ | `pwdnote` | Show the decrypted project note. |
70
+ | `pwdnote init` | Create an encrypted note (`# Project Notes`). |
71
+ | `pwdnote edit` | Decrypt, open in `$VISUAL`/`$EDITOR`, re-encrypt on save. |
72
+ | `pwdnote add "text"` | Append `- text` to the note without opening an editor. |
73
+ | `pwdnote status` | Show the project root, note file, and encryption status. |
74
+ | `pwdnote gitignore` | Add recommended ignore entries (`.pwdnote.tmp`, `.pwdnote.cache`). |
75
+
76
+ ### Examples
77
+
78
+ ```bash
79
+ $ pwdnote
80
+ TODO:
81
+ - rotate AWS keys
82
+ - update deployment docs
83
+ Notes:
84
+ Client requested staging environment.
85
+
86
+ $ pwdnote status
87
+ Project root:
88
+ ~/projects/example
89
+ Note file:
90
+ .pwdnote.enc
91
+ Encrypted:
92
+ Yes
93
+ ```
94
+
95
+ If no note exists yet:
96
+
97
+ ```
98
+ No project note found.
99
+ Run:
100
+ pwdnote init
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Project root detection
106
+
107
+ `pwdnote` does not operate only on the current directory. Starting from your
108
+ working directory it searches **upward**:
109
+
110
+ 1. If `.pwdnote.enc` exists, that location is used.
111
+ 2. Otherwise, if `.git` exists, that location is treated as the project root.
112
+ 3. The search stops at the filesystem root.
113
+
114
+ So from `project/backend/api`, running `pwdnote` finds
115
+ `project/.pwdnote.enc`.
116
+
117
+ ---
118
+
119
+ ## Security model
120
+
121
+ - **Authenticated encryption.** Notes are encrypted with
122
+ [Fernet](https://cryptography.io/en/latest/fernet/) (AES-128-CBC with an
123
+ HMAC-SHA256 authentication tag) from the well-maintained `cryptography`
124
+ library. We do not implement custom cryptography.
125
+ - **Integrity protection.** Tampered or corrupted files fail to decrypt rather
126
+ than returning garbage.
127
+ - **Key storage.** A single key is generated on first use and stored at
128
+ `~/.config/pwdnote/key` (honouring `XDG_CONFIG_HOME`) with `0600`
129
+ permissions inside a `0700` directory.
130
+ - **No plaintext on disk.** `pwdnote edit` writes to a temporary file with
131
+ restrictive permissions and always deletes it afterwards.
132
+ - **Commit-safe.** `.pwdnote.enc` is meant to be committed; it is ciphertext.
133
+ Do **not** ignore it. (The temporary/cache artifacts are ignored instead.)
134
+
135
+ The crypto backend lives behind a small abstraction (`encrypt_text` /
136
+ `decrypt_text`), so it can be replaced later — and future versions may add
137
+ macOS Keychain, 1Password, `age`, or GPG key backends.
138
+
139
+ ---
140
+
141
+ ## Limitations
142
+
143
+ - The key lives on your machine. If you lose `~/.config/pwdnote/key`, encrypted
144
+ notes cannot be recovered. Back the key up somewhere safe.
145
+ - There is no built-in sync. Sharing a note across machines means sharing the
146
+ same key (e.g. via a secrets manager).
147
+ - One note per project root. `pwdnote` is intentionally simple — no databases,
148
+ no cloud, no plugins, no AI features.
149
+
150
+ ---
151
+
152
+ ## Contributing
153
+
154
+ ```bash
155
+ git clone https://github.com/pwdnote/pwdnote
156
+ cd pwdnote
157
+ uv sync # install deps + dev tools
158
+ uv run pytest # run the test suite
159
+ uv run pwdnote --help # try the CLI from source
160
+ ```
161
+
162
+ Issues and pull requests are welcome. Please keep the tool small and reliable —
163
+ new storage/key backends should slot in behind the existing abstractions.
164
+
165
+ ---
166
+
167
+ ## License
168
+
169
+ [MIT](LICENSE)
@@ -0,0 +1,145 @@
1
+ # pwdnote
2
+
3
+ **Encrypted, project-local notes for your terminal.**
4
+
5
+ `pwdnote` keeps project-specific notes — TODOs, deployment notes, AWS account
6
+ details, session IDs, customer context, reminders — encrypted on disk, right
7
+ next to your code, without ever exposing plaintext inside the repository.
8
+
9
+ It is **local-first**, **encrypted-by-default**, **Git-friendly**, and
10
+ **terminal-native**. The single encrypted file (`.pwdnote.enc`) is safe to
11
+ commit; without your key it is just ciphertext.
12
+
13
+ `pwdnote` is *not* a cloud service, a note-taking app, a password manager, a
14
+ database, or a sync platform. It does one small thing well.
15
+
16
+ ---
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ uv tool install pwdnote
22
+ ```
23
+
24
+ That's it — no further setup. The encryption key is generated automatically on
25
+ first use.
26
+
27
+ ---
28
+
29
+ ## Quick start
30
+
31
+ ```bash
32
+ cd my-project
33
+ pwdnote init # create .pwdnote.enc
34
+ pwdnote edit # open it in your editor
35
+ pwdnote # print the decrypted note
36
+ pwdnote add "Remember to rotate AWS credentials"
37
+ ```
38
+
39
+ ---
40
+
41
+ ## Commands
42
+
43
+ | Command | Description |
44
+ | --- | --- |
45
+ | `pwdnote` | Show the decrypted project note. |
46
+ | `pwdnote init` | Create an encrypted note (`# Project Notes`). |
47
+ | `pwdnote edit` | Decrypt, open in `$VISUAL`/`$EDITOR`, re-encrypt on save. |
48
+ | `pwdnote add "text"` | Append `- text` to the note without opening an editor. |
49
+ | `pwdnote status` | Show the project root, note file, and encryption status. |
50
+ | `pwdnote gitignore` | Add recommended ignore entries (`.pwdnote.tmp`, `.pwdnote.cache`). |
51
+
52
+ ### Examples
53
+
54
+ ```bash
55
+ $ pwdnote
56
+ TODO:
57
+ - rotate AWS keys
58
+ - update deployment docs
59
+ Notes:
60
+ Client requested staging environment.
61
+
62
+ $ pwdnote status
63
+ Project root:
64
+ ~/projects/example
65
+ Note file:
66
+ .pwdnote.enc
67
+ Encrypted:
68
+ Yes
69
+ ```
70
+
71
+ If no note exists yet:
72
+
73
+ ```
74
+ No project note found.
75
+ Run:
76
+ pwdnote init
77
+ ```
78
+
79
+ ---
80
+
81
+ ## Project root detection
82
+
83
+ `pwdnote` does not operate only on the current directory. Starting from your
84
+ working directory it searches **upward**:
85
+
86
+ 1. If `.pwdnote.enc` exists, that location is used.
87
+ 2. Otherwise, if `.git` exists, that location is treated as the project root.
88
+ 3. The search stops at the filesystem root.
89
+
90
+ So from `project/backend/api`, running `pwdnote` finds
91
+ `project/.pwdnote.enc`.
92
+
93
+ ---
94
+
95
+ ## Security model
96
+
97
+ - **Authenticated encryption.** Notes are encrypted with
98
+ [Fernet](https://cryptography.io/en/latest/fernet/) (AES-128-CBC with an
99
+ HMAC-SHA256 authentication tag) from the well-maintained `cryptography`
100
+ library. We do not implement custom cryptography.
101
+ - **Integrity protection.** Tampered or corrupted files fail to decrypt rather
102
+ than returning garbage.
103
+ - **Key storage.** A single key is generated on first use and stored at
104
+ `~/.config/pwdnote/key` (honouring `XDG_CONFIG_HOME`) with `0600`
105
+ permissions inside a `0700` directory.
106
+ - **No plaintext on disk.** `pwdnote edit` writes to a temporary file with
107
+ restrictive permissions and always deletes it afterwards.
108
+ - **Commit-safe.** `.pwdnote.enc` is meant to be committed; it is ciphertext.
109
+ Do **not** ignore it. (The temporary/cache artifacts are ignored instead.)
110
+
111
+ The crypto backend lives behind a small abstraction (`encrypt_text` /
112
+ `decrypt_text`), so it can be replaced later — and future versions may add
113
+ macOS Keychain, 1Password, `age`, or GPG key backends.
114
+
115
+ ---
116
+
117
+ ## Limitations
118
+
119
+ - The key lives on your machine. If you lose `~/.config/pwdnote/key`, encrypted
120
+ notes cannot be recovered. Back the key up somewhere safe.
121
+ - There is no built-in sync. Sharing a note across machines means sharing the
122
+ same key (e.g. via a secrets manager).
123
+ - One note per project root. `pwdnote` is intentionally simple — no databases,
124
+ no cloud, no plugins, no AI features.
125
+
126
+ ---
127
+
128
+ ## Contributing
129
+
130
+ ```bash
131
+ git clone https://github.com/pwdnote/pwdnote
132
+ cd pwdnote
133
+ uv sync # install deps + dev tools
134
+ uv run pytest # run the test suite
135
+ uv run pwdnote --help # try the CLI from source
136
+ ```
137
+
138
+ Issues and pull requests are welcome. Please keep the tool small and reliable —
139
+ new storage/key backends should slot in behind the existing abstractions.
140
+
141
+ ---
142
+
143
+ ## License
144
+
145
+ [MIT](LICENSE)
@@ -0,0 +1,52 @@
1
+ [project]
2
+ name = "pwdnote"
3
+ version = "0.1.0"
4
+ description = "Encrypted, project-local notes for your terminal."
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "pwdnote maintainers" }]
9
+ keywords = ["notes", "encryption", "cli", "terminal", "project"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Environment :: Console",
13
+ "Intended Audience :: Developers",
14
+ "License :: OSI Approved :: MIT License",
15
+ "Programming Language :: Python :: 3.12",
16
+ "Programming Language :: Python :: 3.13",
17
+ "Topic :: Utilities",
18
+ ]
19
+ dependencies = [
20
+ "typer>=0.12",
21
+ "rich>=13.7",
22
+ "cryptography>=42.0",
23
+ ]
24
+
25
+ [project.urls]
26
+ Homepage = "https://github.com/pwdnote/pwdnote"
27
+ Repository = "https://github.com/pwdnote/pwdnote"
28
+ Issues = "https://github.com/pwdnote/pwdnote/issues"
29
+
30
+ [project.scripts]
31
+ pwdnote = "pwdnote.cli:app"
32
+
33
+ [build-system]
34
+ requires = ["hatchling"]
35
+ build-backend = "hatchling.build"
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ packages = ["src/pwdnote"]
39
+
40
+ [dependency-groups]
41
+ dev = [
42
+ "pytest>=8",
43
+ "pytest-cov>=5",
44
+ ]
45
+
46
+ [tool.pytest.ini_options]
47
+ testpaths = ["tests"]
48
+ addopts = "-q"
49
+
50
+ [tool.coverage.run]
51
+ branch = true
52
+ source = ["pwdnote"]
@@ -0,0 +1,3 @@
1
+ """pwdnote — encrypted, project-local notes for your terminal."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,147 @@
1
+ """Command-line interface for pwdnote."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import NoReturn
7
+
8
+ import typer
9
+ from rich.console import Console
10
+
11
+ from . import editor as editor_mod
12
+ from . import notes, project
13
+ from .config import load_or_create_key
14
+ from .crypto import DecryptionError
15
+
16
+ app = typer.Typer(
17
+ name="pwdnote",
18
+ help="Encrypted, project-local notes for your terminal.",
19
+ no_args_is_help=False,
20
+ add_completion=False,
21
+ )
22
+
23
+ console = Console()
24
+
25
+
26
+ def _fail(message: str) -> NoReturn:
27
+ console.print(message)
28
+ raise typer.Exit(code=1)
29
+
30
+
31
+ def _no_note() -> NoReturn:
32
+ console.print("No project note found.")
33
+ console.print("Run:")
34
+ console.print(" pwdnote init")
35
+ raise typer.Exit(code=1)
36
+
37
+
38
+ def _read_existing() -> tuple[Path, bytes, str]:
39
+ """Locate the note, load the key, and decrypt — or fail with a message."""
40
+ note_path = project.find_existing_note(Path.cwd())
41
+ if note_path is None:
42
+ _no_note()
43
+ key = load_or_create_key()
44
+ try:
45
+ text = notes.read_note(note_path, key)
46
+ except DecryptionError:
47
+ _fail("Unable to decrypt project note.")
48
+ except PermissionError:
49
+ _fail("Unable to access note file.")
50
+ return note_path, key, text
51
+
52
+
53
+ @app.callback(invoke_without_command=True)
54
+ def main(ctx: typer.Context) -> None:
55
+ """Show the decrypted project note when no subcommand is given."""
56
+ if ctx.invoked_subcommand is not None:
57
+ return
58
+ _, _, text = _read_existing()
59
+ console.print(text, end="" if text.endswith("\n") else "\n", highlight=False)
60
+
61
+
62
+ @app.command()
63
+ def init() -> None:
64
+ """Create an encrypted project note."""
65
+ root = project.resolve_project_root(Path.cwd())
66
+ note_path = project.note_path_for(root)
67
+ key = load_or_create_key()
68
+ try:
69
+ notes.init_note(note_path, key)
70
+ except notes.NoteExistsError:
71
+ _fail("Project note already exists.")
72
+ except PermissionError:
73
+ _fail("Unable to access note file.")
74
+ console.print(f"Created {note_path}")
75
+
76
+
77
+ @app.command()
78
+ def edit() -> None:
79
+ """Edit the project note in your editor."""
80
+ note_path, key, text = _read_existing()
81
+ edited = editor_mod.edit_text(text, note_path.parent)
82
+ try:
83
+ notes.write_note(note_path, key, edited)
84
+ except PermissionError:
85
+ _fail("Unable to access note file.")
86
+ console.print("Note saved.")
87
+
88
+
89
+ @app.command()
90
+ def add(text: str = typer.Argument(..., help="Text to append as a bullet point.")) -> None:
91
+ """Append a line to the project note without opening an editor."""
92
+ note_path, key, _ = _read_existing()
93
+ try:
94
+ notes.append_line(note_path, key, text)
95
+ except PermissionError:
96
+ _fail("Unable to access note file.")
97
+ console.print(f"Added: - {text}")
98
+
99
+
100
+ @app.command()
101
+ def status() -> None:
102
+ """Show the project root, note file, and encryption status."""
103
+ start = Path.cwd()
104
+ note_path = project.find_existing_note(start)
105
+ if note_path is None:
106
+ root = project.resolve_project_root(start)
107
+ console.print("Project root:")
108
+ console.print(f" {root}")
109
+ console.print("Note file:")
110
+ console.print(" (none — run 'pwdnote init')")
111
+ console.print("Encrypted:")
112
+ console.print(" No note yet")
113
+ return
114
+ console.print("Project root:")
115
+ console.print(f" {note_path.parent}")
116
+ console.print("Note file:")
117
+ console.print(f" {note_path.name}")
118
+ console.print("Encrypted:")
119
+ console.print(" Yes")
120
+
121
+
122
+ @app.command()
123
+ def gitignore() -> None:
124
+ """Add recommended pwdnote entries to the project's .gitignore."""
125
+ root = project.resolve_project_root(Path.cwd())
126
+ gitignore_path = root / ".gitignore"
127
+ recommended = [".pwdnote.tmp", ".pwdnote.cache"]
128
+
129
+ content = gitignore_path.read_text(encoding="utf-8") if gitignore_path.exists() else ""
130
+ existing = set(content.splitlines())
131
+ to_add = [entry for entry in recommended if entry not in existing]
132
+
133
+ if not to_add:
134
+ console.print("All recommended entries are already present.")
135
+ return
136
+
137
+ prefix = "" if content == "" or content.endswith("\n") else "\n"
138
+ with gitignore_path.open("a", encoding="utf-8") as handle:
139
+ handle.write(prefix + "".join(f"{entry}\n" for entry in to_add))
140
+
141
+ console.print(f"Added to {gitignore_path}:")
142
+ for entry in to_add:
143
+ console.print(f" {entry}")
144
+
145
+
146
+ if __name__ == "__main__": # pragma: no cover
147
+ app()
@@ -0,0 +1,61 @@
1
+ """Key management and configuration.
2
+
3
+ Version 1 stores a single auto-generated key on disk with restrictive
4
+ permissions. The lookup is structured so alternative backends (macOS Keychain,
5
+ 1Password, age, GPG) can be added later behind ``load_or_create_key``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from pathlib import Path
12
+
13
+ from .crypto import generate_key
14
+
15
+
16
+ def get_config_dir() -> Path:
17
+ """Return the directory that holds pwdnote configuration and the key.
18
+
19
+ Honours ``PWDNOTE_CONFIG_DIR`` and ``XDG_CONFIG_HOME`` overrides, falling
20
+ back to ``~/.config/pwdnote``.
21
+ """
22
+ override = os.environ.get("PWDNOTE_CONFIG_DIR")
23
+ if override:
24
+ return Path(override)
25
+ xdg = os.environ.get("XDG_CONFIG_HOME")
26
+ if xdg:
27
+ return Path(xdg) / "pwdnote"
28
+ return Path.home() / ".config" / "pwdnote"
29
+
30
+
31
+ def get_key_path() -> Path:
32
+ """Return the path to the encryption key file."""
33
+ override = os.environ.get("PWDNOTE_KEY_FILE")
34
+ if override:
35
+ return Path(override)
36
+ return get_config_dir() / "key"
37
+
38
+
39
+ def load_or_create_key() -> bytes:
40
+ """Load the encryption key, creating it on first use.
41
+
42
+ The key file is created with ``0600`` permissions inside a ``0700``
43
+ directory so that other users on the system cannot read it.
44
+ """
45
+ path = get_key_path()
46
+ if path.exists():
47
+ return path.read_bytes().strip()
48
+
49
+ path.parent.mkdir(parents=True, exist_ok=True)
50
+ try:
51
+ os.chmod(path.parent, 0o700)
52
+ except OSError:
53
+ # Best effort; not all filesystems support chmod.
54
+ pass
55
+
56
+ key = generate_key()
57
+ # O_EXCL guards against a concurrent writer; 0o600 keeps it private.
58
+ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
59
+ with os.fdopen(fd, "wb") as handle:
60
+ handle.write(key)
61
+ return key