private-files 1.0.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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Samuel J. McKelvie
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,273 @@
1
+ Metadata-Version: 2.1
2
+ Name: private-files
3
+ Version: 1.0.0
4
+ Summary: Manage secret/private files in an os-independent way
5
+ Keywords: private,secrets,credentials,security,permissions,config,cross-platform,filesystem,dotfiles,application-data
6
+ Author-Email: Sam McKelvie <dev@mckelvie.org>
7
+ License: MIT License
8
+
9
+ Copyright (c) 2026 Samuel J. McKelvie
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+
29
+ Classifier: Development Status :: 5 - Production/Stable
30
+ Classifier: Intended Audience :: Developers
31
+ Classifier: License :: OSI Approved :: MIT License
32
+ Classifier: Operating System :: OS Independent
33
+ Classifier: Programming Language :: Python :: 3
34
+ Classifier: Programming Language :: Python :: 3.10
35
+ Classifier: Programming Language :: Python :: 3.11
36
+ Classifier: Programming Language :: Python :: 3.12
37
+ Classifier: Programming Language :: Python :: 3.13
38
+ Classifier: Programming Language :: Python :: 3.14
39
+ Classifier: Topic :: Security
40
+ Classifier: Topic :: System :: Filesystems
41
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
42
+ Classifier: Typing :: Typed
43
+ Project-URL: Homepage, https://github.com/mckelvie-org/private-files
44
+ Project-URL: Source, https://github.com/mckelvie-org/private-files/tree/v1.0.0
45
+ Project-URL: Bug Tracker, https://github.com/mckelvie-org/private-files/issues
46
+ Project-URL: Changelog, https://github.com/mckelvie-org/private-files/releases
47
+ Requires-Python: >=3.10
48
+ Requires-Dist: platformdirs>=4.0.0
49
+ Description-Content-Type: text/markdown
50
+
51
+ # private-files
52
+
53
+ [![CI](https://img.shields.io/badge/CI-passing-brightgreen.svg)](https://github.com/mckelvie-org/private-files/actions/workflows/ci.yml)
54
+ [![PyPI version](https://img.shields.io/badge/pypi-v1.0.0-blue.svg)](https://pypi.org/project/private-files/1.0.0/)
55
+ [![Python versions](https://img.shields.io/badge/python-3.10%20|%203.11%20|%203.12%20|%203.13%20|%203.14-blue.svg)](https://pypi.org/project/private-files/)
56
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
57
+
58
+ `private-files`: Manage secret/private files in an os-independent way.
59
+
60
+ Applications that need to persist sensitive data on a user's machine &mdash; API tokens, session cookies,
61
+ credentials, profile data &mdash; need somewhere to put it that isn't the world-readable home directory
62
+ clutter of `~/.config` or `~/.myapp`. `private-files` gives every application its own subdirectory of a
63
+ single, locked-down, user-wide private root, and enforces `0700`/`0600` permissions on directories and
64
+ files it creates so secrets are never accidentally left group- or world-readable.
65
+
66
+ ## Highlights
67
+
68
+ - **One shared private root per user.** On Linux and macOS this is `~/.private`; on Windows it's the
69
+ non-roaming application-data directory (via [`platformdirs`](https://pypi.org/project/platformdirs/)),
70
+ which is not synced to the cloud or shared across devices.
71
+ - **Per-application subdirectories.** Each app gets its own subdirectory of the shared root, named after
72
+ an `app_name` you choose, so multiple applications can share the same machine without stepping on each
73
+ other's secrets.
74
+ - **Permissions are enforced, not assumed.** Directories created by this package are `chmod 0700`; files
75
+ opened for writing are `chmod 0600`. Existing app-specific directories with the wrong permissions are
76
+ fixed automatically; the shared root itself is only ever checked, never silently "fixed," since it may
77
+ be shared with other applications.
78
+ - **Path-traversal safe.** Subdirectory and filename arguments are resolved and checked to ensure they
79
+ stay within the intended app directory &mdash; a `subdir` or `filename` of `"../../etc/passwd"` raises
80
+ `ValueError` instead of silently escaping the sandbox.
81
+ - **Two equivalent APIs.** A `PrivateFilesManager` class for when you're working with one application
82
+ repeatedly (it caches the resolved paths), and a set of flat module-level functions for one-off calls,
83
+ both backed by the same cached manager instances.
84
+ - **Drop-in `open()` replacement.** `open_private_app_file()` / `PrivateFilesManager.open()` behave like
85
+ the builtin `open()` &mdash; including `@overload`-based mode-based return-type inference (`TextIO` vs
86
+ `BinaryIO`) &mdash; but resolve the path into the private directory and create parent directories and
87
+ fix file permissions automatically.
88
+ - **Fully typed**, `mypy --strict` clean, zero required dependencies beyond `platformdirs`.
89
+
90
+ ## Installation
91
+
92
+ ```bash
93
+ pip install private-files
94
+ ```
95
+
96
+ ## Quick Start
97
+
98
+ ```python
99
+ from private_files import open_private_app_file, get_private_app_dir
100
+
101
+ # Write a secret. Parent directories are created automatically (mode 0700),
102
+ # and the file itself ends up with mode 0600.
103
+ with open_private_app_file("api-token.txt", "w", app_name="myapp") as f:
104
+ f.write("super-secret-token")
105
+
106
+ # Read it back later.
107
+ with open_private_app_file("api-token.txt", "r", app_name="myapp") as f:
108
+ token = f.read()
109
+
110
+ # Find out where it lives on disk, without opening it.
111
+ print(get_private_app_dir(app_name="myapp"))
112
+ # -> /home/alice/.private/myapp (Linux/macOS)
113
+ # -> C:\Users\alice\AppData\Local\myapp\myapp (Windows)
114
+ ```
115
+
116
+ If your application makes several calls, prefer a `PrivateFilesManager`, which resolves and caches its
117
+ paths once instead of on every call:
118
+
119
+ ```python
120
+ from private_files import PrivateFilesManager
121
+
122
+ files = PrivateFilesManager(app_name="myapp")
123
+
124
+ with files.open("api-token.txt", "w") as f:
125
+ f.write("super-secret-token")
126
+
127
+ with files.open("session/cookies.json", "w", create_parent=True) as f:
128
+ f.write("{}")
129
+
130
+ files.delete_private_dir("session")
131
+ ```
132
+
133
+ `get_private_files_manager(app_name)` returns a process-wide cached `PrivateFilesManager` for a given
134
+ `app_name`, which is what all of the flat module-level functions use internally &mdash; so mixing the two
135
+ styles for the same `app_name` shares the same cached, verified paths.
136
+
137
+ ## Concepts
138
+
139
+ ### Shared root vs. app-specific directory
140
+
141
+ There are two levels of directory:
142
+
143
+ - The **shared private root** is one directory per user, shared by every application using this package:
144
+ `~/.private` on Linux/macOS, or the non-roaming app-data directory on Windows. It must already have
145
+ (or be given) permissions `0700`. This package will create it if missing but will **not** silently fix
146
+ its permissions if it already exists with the wrong mode, since it may be shared with other
147
+ applications you don't control &mdash; instead it raises `PermissionError` so you can decide what to do.
148
+ - The **application-specific directory** is a subdirectory of the shared root named after your `app_name`
149
+ (e.g. `~/.private/myapp`). Unlike the shared root, this package **does** own it, so it actively enforces
150
+ and repairs `0700` permissions on it and any subdirectories you create within it.
151
+
152
+ Passing `app_name=None` (the default) targets the shared root itself rather than a per-application
153
+ subdirectory.
154
+
155
+ ### Subdirectories and files
156
+
157
+ Within an application's directory you can create arbitrarily nested subdirectories (`subdir="cache/v2"`)
158
+ and files within them. Every directory component created by `create_private_dir()` (or implicitly by
159
+ `create_parent=True`) gets its permissions verified and, if necessary, corrected to `0700`. Every
160
+ directory checked by `verify_private_dir()` must already be `0700`, or a `PermissionError` is raised.
161
+
162
+ All `subdir` and `filename` arguments are resolved and checked against the directory they're supposed to
163
+ be contained within; anything that would resolve outside of it (via `..`, absolute paths outside the
164
+ tree, symlinks, etc.) raises `ValueError` rather than being silently permitted.
165
+
166
+ ## API Reference
167
+
168
+ ### `PrivateFilesManager`
169
+
170
+ ```python
171
+ class PrivateFilesManager:
172
+ def __init__(self, app_name: str | None = None): ...
173
+ ```
174
+
175
+ An object bound to a single `app_name` (or `None` for the shared root) that resolves and caches its
176
+ directory paths across calls.
177
+
178
+ | Method | Description |
179
+ | --- | --- |
180
+ | `get_shared_root_dir() -> Path` | The shared private root (e.g. `~/.private`). Computed, not created. |
181
+ | `create_shared_root_dir() -> Path` | Create the shared root if missing (mode `0700`); raise `PermissionError` if it exists with the wrong mode. |
182
+ | `get_root_dir() -> Path` | This manager's app-specific directory. Computed, not created. |
183
+ | `create_root_dir() -> Path` | Create the app-specific directory (and the shared root, if needed), fixing permissions at every level. |
184
+ | `get_private_dir(subdir) -> Path` | Resolve `subdir` under the app directory. Does not create anything. |
185
+ | `create_private_dir(subdir) -> Path` | Create `subdir` (and every intermediate component) under the app directory, mode `0700`. |
186
+ | `delete_private_dir(subdir) -> None` | Recursively delete `subdir`. No-op if it doesn't exist. Raises `ValueError` if `subdir` resolves to the shared root itself. |
187
+ | `verify_private_dir(subdir) -> Path` | Raise `NotADirectoryError`/`PermissionError` unless `subdir` (and everything above it, up to the shared root) exists with mode `0700`. |
188
+ | `get_private_file(filename, *, create_parent=False, subdir=".") -> Path` | Resolve the full path to a file. Verifies (or creates, if `create_parent=True`) its parent directory. The file itself is never created. |
189
+ | `open(filename, mode="r", *, subdir=".", create_parent=False, **kwargs) -> IO` | Like builtin `open()`, but resolved into the app directory. Parent directories are auto-created for write/append/exclusive-create modes (or when `create_parent=True`); files opened for writing get mode `0600`. |
190
+
191
+ `subdir="."` refers to the app directory itself. All `subdir`/`filename` parameters accept `str | Path`.
192
+
193
+ ### Module-level functions
194
+
195
+ Thin wrappers around a cached `PrivateFilesManager` per `app_name`, for callers that don't want to hold
196
+ onto a manager instance themselves:
197
+
198
+ | Function | Equivalent to |
199
+ | --- | --- |
200
+ | `get_shared_private_dir() -> Path` | `PrivateFilesManager().get_shared_root_dir()` |
201
+ | `create_shared_private_dir() -> Path` | `PrivateFilesManager().create_shared_root_dir()` |
202
+ | `get_private_files_manager(app_name=None) -> PrivateFilesManager` | Returns the cached manager for `app_name`. |
203
+ | `get_private_app_dir(app_name=None) -> Path` | `manager.get_root_dir()` |
204
+ | `create_private_app_dir(app_name=None) -> Path` | `manager.create_root_dir()` |
205
+ | `get_private_dir(subdir, app_name=None) -> Path` | `manager.get_private_dir(subdir)` |
206
+ | `create_private_dir(subdir, app_name=None) -> Path` | `manager.create_private_dir(subdir)` |
207
+ | `delete_private_dir(subdir_name, app_name) -> None` | `manager.delete_private_dir(subdir_name)` |
208
+ | `verify_private_dir(subdir_name, app_name=None) -> Path` | `manager.verify_private_dir(subdir_name)` |
209
+ | `get_private_app_file(filename, *, create_parent=False, subdir=".", app_name=None) -> Path` | `manager.get_private_file(...)` |
210
+ | `open_private_app_file(filename, mode="r", *, subdir=".", create_parent=False, app_name=None, **kwargs) -> IO` | `manager.open(...)` |
211
+
212
+ `get_private_files_manager(app_name)` is `@functools.cache`d, so repeated calls with the same `app_name`
213
+ (directly, or indirectly via any of the flat functions above) return the same manager instance and reuse
214
+ its already-resolved, already-verified paths. This means the *first* successful resolution of a given
215
+ `app_name`'s directory sticks for the lifetime of the process, even if the underlying shared root were to
216
+ change (e.g. `$HOME` changing at runtime) &mdash; construct a fresh `PrivateFilesManager` directly if you
217
+ need to bypass the cache.
218
+
219
+ ## Examples
220
+
221
+ ### Reading and writing binary data
222
+
223
+ ```python
224
+ from private_files import open_private_app_file
225
+
226
+ with open_private_app_file("cache.bin", "wb", app_name="myapp") as f:
227
+ f.write(b"\x00\x01\x02")
228
+
229
+ with open_private_app_file("cache.bin", "rb", app_name="myapp") as f:
230
+ data = f.read()
231
+ ```
232
+
233
+ ### Nested subdirectories
234
+
235
+ ```python
236
+ from private_files import PrivateFilesManager
237
+
238
+ files = PrivateFilesManager(app_name="myapp")
239
+ files.create_private_dir("sessions/2024") # creates both levels, each mode 0700
240
+ path = files.get_private_dir("sessions/2024")
241
+ ```
242
+
243
+ ### Checking a directory without creating it
244
+
245
+ ```python
246
+ from private_files import verify_private_dir
247
+
248
+ try:
249
+ verify_private_dir("sessions", app_name="myapp")
250
+ except (NotADirectoryError, PermissionError) as e:
251
+ print(f"not ready: {e}")
252
+ ```
253
+
254
+ ### Cleaning up
255
+
256
+ ```python
257
+ from private_files import delete_private_dir
258
+
259
+ # Removes the directory and everything under it. No error if it doesn't exist.
260
+ delete_private_dir("sessions", app_name="myapp")
261
+ ```
262
+
263
+ ## Supported Python Versions
264
+
265
+ Python 3.10 through 3.14.
266
+
267
+ ## License
268
+
269
+ MIT. See [LICENSE](LICENSE).
270
+
271
+ ---
272
+
273
+ For development and release workflow documentation, see [CONTRIBUTING.md](CONTRIBUTING.md).
@@ -0,0 +1,223 @@
1
+ # private-files
2
+
3
+ [![CI](https://img.shields.io/badge/CI-passing-brightgreen.svg)](https://github.com/mckelvie-org/private-files/actions/workflows/ci.yml)
4
+ [![PyPI version](https://img.shields.io/badge/pypi-v1.0.0-blue.svg)](https://pypi.org/project/private-files/1.0.0/)
5
+ [![Python versions](https://img.shields.io/badge/python-3.10%20|%203.11%20|%203.12%20|%203.13%20|%203.14-blue.svg)](https://pypi.org/project/private-files/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
+
8
+ `private-files`: Manage secret/private files in an os-independent way.
9
+
10
+ Applications that need to persist sensitive data on a user's machine &mdash; API tokens, session cookies,
11
+ credentials, profile data &mdash; need somewhere to put it that isn't the world-readable home directory
12
+ clutter of `~/.config` or `~/.myapp`. `private-files` gives every application its own subdirectory of a
13
+ single, locked-down, user-wide private root, and enforces `0700`/`0600` permissions on directories and
14
+ files it creates so secrets are never accidentally left group- or world-readable.
15
+
16
+ ## Highlights
17
+
18
+ - **One shared private root per user.** On Linux and macOS this is `~/.private`; on Windows it's the
19
+ non-roaming application-data directory (via [`platformdirs`](https://pypi.org/project/platformdirs/)),
20
+ which is not synced to the cloud or shared across devices.
21
+ - **Per-application subdirectories.** Each app gets its own subdirectory of the shared root, named after
22
+ an `app_name` you choose, so multiple applications can share the same machine without stepping on each
23
+ other's secrets.
24
+ - **Permissions are enforced, not assumed.** Directories created by this package are `chmod 0700`; files
25
+ opened for writing are `chmod 0600`. Existing app-specific directories with the wrong permissions are
26
+ fixed automatically; the shared root itself is only ever checked, never silently "fixed," since it may
27
+ be shared with other applications.
28
+ - **Path-traversal safe.** Subdirectory and filename arguments are resolved and checked to ensure they
29
+ stay within the intended app directory &mdash; a `subdir` or `filename` of `"../../etc/passwd"` raises
30
+ `ValueError` instead of silently escaping the sandbox.
31
+ - **Two equivalent APIs.** A `PrivateFilesManager` class for when you're working with one application
32
+ repeatedly (it caches the resolved paths), and a set of flat module-level functions for one-off calls,
33
+ both backed by the same cached manager instances.
34
+ - **Drop-in `open()` replacement.** `open_private_app_file()` / `PrivateFilesManager.open()` behave like
35
+ the builtin `open()` &mdash; including `@overload`-based mode-based return-type inference (`TextIO` vs
36
+ `BinaryIO`) &mdash; but resolve the path into the private directory and create parent directories and
37
+ fix file permissions automatically.
38
+ - **Fully typed**, `mypy --strict` clean, zero required dependencies beyond `platformdirs`.
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install private-files
44
+ ```
45
+
46
+ ## Quick Start
47
+
48
+ ```python
49
+ from private_files import open_private_app_file, get_private_app_dir
50
+
51
+ # Write a secret. Parent directories are created automatically (mode 0700),
52
+ # and the file itself ends up with mode 0600.
53
+ with open_private_app_file("api-token.txt", "w", app_name="myapp") as f:
54
+ f.write("super-secret-token")
55
+
56
+ # Read it back later.
57
+ with open_private_app_file("api-token.txt", "r", app_name="myapp") as f:
58
+ token = f.read()
59
+
60
+ # Find out where it lives on disk, without opening it.
61
+ print(get_private_app_dir(app_name="myapp"))
62
+ # -> /home/alice/.private/myapp (Linux/macOS)
63
+ # -> C:\Users\alice\AppData\Local\myapp\myapp (Windows)
64
+ ```
65
+
66
+ If your application makes several calls, prefer a `PrivateFilesManager`, which resolves and caches its
67
+ paths once instead of on every call:
68
+
69
+ ```python
70
+ from private_files import PrivateFilesManager
71
+
72
+ files = PrivateFilesManager(app_name="myapp")
73
+
74
+ with files.open("api-token.txt", "w") as f:
75
+ f.write("super-secret-token")
76
+
77
+ with files.open("session/cookies.json", "w", create_parent=True) as f:
78
+ f.write("{}")
79
+
80
+ files.delete_private_dir("session")
81
+ ```
82
+
83
+ `get_private_files_manager(app_name)` returns a process-wide cached `PrivateFilesManager` for a given
84
+ `app_name`, which is what all of the flat module-level functions use internally &mdash; so mixing the two
85
+ styles for the same `app_name` shares the same cached, verified paths.
86
+
87
+ ## Concepts
88
+
89
+ ### Shared root vs. app-specific directory
90
+
91
+ There are two levels of directory:
92
+
93
+ - The **shared private root** is one directory per user, shared by every application using this package:
94
+ `~/.private` on Linux/macOS, or the non-roaming app-data directory on Windows. It must already have
95
+ (or be given) permissions `0700`. This package will create it if missing but will **not** silently fix
96
+ its permissions if it already exists with the wrong mode, since it may be shared with other
97
+ applications you don't control &mdash; instead it raises `PermissionError` so you can decide what to do.
98
+ - The **application-specific directory** is a subdirectory of the shared root named after your `app_name`
99
+ (e.g. `~/.private/myapp`). Unlike the shared root, this package **does** own it, so it actively enforces
100
+ and repairs `0700` permissions on it and any subdirectories you create within it.
101
+
102
+ Passing `app_name=None` (the default) targets the shared root itself rather than a per-application
103
+ subdirectory.
104
+
105
+ ### Subdirectories and files
106
+
107
+ Within an application's directory you can create arbitrarily nested subdirectories (`subdir="cache/v2"`)
108
+ and files within them. Every directory component created by `create_private_dir()` (or implicitly by
109
+ `create_parent=True`) gets its permissions verified and, if necessary, corrected to `0700`. Every
110
+ directory checked by `verify_private_dir()` must already be `0700`, or a `PermissionError` is raised.
111
+
112
+ All `subdir` and `filename` arguments are resolved and checked against the directory they're supposed to
113
+ be contained within; anything that would resolve outside of it (via `..`, absolute paths outside the
114
+ tree, symlinks, etc.) raises `ValueError` rather than being silently permitted.
115
+
116
+ ## API Reference
117
+
118
+ ### `PrivateFilesManager`
119
+
120
+ ```python
121
+ class PrivateFilesManager:
122
+ def __init__(self, app_name: str | None = None): ...
123
+ ```
124
+
125
+ An object bound to a single `app_name` (or `None` for the shared root) that resolves and caches its
126
+ directory paths across calls.
127
+
128
+ | Method | Description |
129
+ | --- | --- |
130
+ | `get_shared_root_dir() -> Path` | The shared private root (e.g. `~/.private`). Computed, not created. |
131
+ | `create_shared_root_dir() -> Path` | Create the shared root if missing (mode `0700`); raise `PermissionError` if it exists with the wrong mode. |
132
+ | `get_root_dir() -> Path` | This manager's app-specific directory. Computed, not created. |
133
+ | `create_root_dir() -> Path` | Create the app-specific directory (and the shared root, if needed), fixing permissions at every level. |
134
+ | `get_private_dir(subdir) -> Path` | Resolve `subdir` under the app directory. Does not create anything. |
135
+ | `create_private_dir(subdir) -> Path` | Create `subdir` (and every intermediate component) under the app directory, mode `0700`. |
136
+ | `delete_private_dir(subdir) -> None` | Recursively delete `subdir`. No-op if it doesn't exist. Raises `ValueError` if `subdir` resolves to the shared root itself. |
137
+ | `verify_private_dir(subdir) -> Path` | Raise `NotADirectoryError`/`PermissionError` unless `subdir` (and everything above it, up to the shared root) exists with mode `0700`. |
138
+ | `get_private_file(filename, *, create_parent=False, subdir=".") -> Path` | Resolve the full path to a file. Verifies (or creates, if `create_parent=True`) its parent directory. The file itself is never created. |
139
+ | `open(filename, mode="r", *, subdir=".", create_parent=False, **kwargs) -> IO` | Like builtin `open()`, but resolved into the app directory. Parent directories are auto-created for write/append/exclusive-create modes (or when `create_parent=True`); files opened for writing get mode `0600`. |
140
+
141
+ `subdir="."` refers to the app directory itself. All `subdir`/`filename` parameters accept `str | Path`.
142
+
143
+ ### Module-level functions
144
+
145
+ Thin wrappers around a cached `PrivateFilesManager` per `app_name`, for callers that don't want to hold
146
+ onto a manager instance themselves:
147
+
148
+ | Function | Equivalent to |
149
+ | --- | --- |
150
+ | `get_shared_private_dir() -> Path` | `PrivateFilesManager().get_shared_root_dir()` |
151
+ | `create_shared_private_dir() -> Path` | `PrivateFilesManager().create_shared_root_dir()` |
152
+ | `get_private_files_manager(app_name=None) -> PrivateFilesManager` | Returns the cached manager for `app_name`. |
153
+ | `get_private_app_dir(app_name=None) -> Path` | `manager.get_root_dir()` |
154
+ | `create_private_app_dir(app_name=None) -> Path` | `manager.create_root_dir()` |
155
+ | `get_private_dir(subdir, app_name=None) -> Path` | `manager.get_private_dir(subdir)` |
156
+ | `create_private_dir(subdir, app_name=None) -> Path` | `manager.create_private_dir(subdir)` |
157
+ | `delete_private_dir(subdir_name, app_name) -> None` | `manager.delete_private_dir(subdir_name)` |
158
+ | `verify_private_dir(subdir_name, app_name=None) -> Path` | `manager.verify_private_dir(subdir_name)` |
159
+ | `get_private_app_file(filename, *, create_parent=False, subdir=".", app_name=None) -> Path` | `manager.get_private_file(...)` |
160
+ | `open_private_app_file(filename, mode="r", *, subdir=".", create_parent=False, app_name=None, **kwargs) -> IO` | `manager.open(...)` |
161
+
162
+ `get_private_files_manager(app_name)` is `@functools.cache`d, so repeated calls with the same `app_name`
163
+ (directly, or indirectly via any of the flat functions above) return the same manager instance and reuse
164
+ its already-resolved, already-verified paths. This means the *first* successful resolution of a given
165
+ `app_name`'s directory sticks for the lifetime of the process, even if the underlying shared root were to
166
+ change (e.g. `$HOME` changing at runtime) &mdash; construct a fresh `PrivateFilesManager` directly if you
167
+ need to bypass the cache.
168
+
169
+ ## Examples
170
+
171
+ ### Reading and writing binary data
172
+
173
+ ```python
174
+ from private_files import open_private_app_file
175
+
176
+ with open_private_app_file("cache.bin", "wb", app_name="myapp") as f:
177
+ f.write(b"\x00\x01\x02")
178
+
179
+ with open_private_app_file("cache.bin", "rb", app_name="myapp") as f:
180
+ data = f.read()
181
+ ```
182
+
183
+ ### Nested subdirectories
184
+
185
+ ```python
186
+ from private_files import PrivateFilesManager
187
+
188
+ files = PrivateFilesManager(app_name="myapp")
189
+ files.create_private_dir("sessions/2024") # creates both levels, each mode 0700
190
+ path = files.get_private_dir("sessions/2024")
191
+ ```
192
+
193
+ ### Checking a directory without creating it
194
+
195
+ ```python
196
+ from private_files import verify_private_dir
197
+
198
+ try:
199
+ verify_private_dir("sessions", app_name="myapp")
200
+ except (NotADirectoryError, PermissionError) as e:
201
+ print(f"not ready: {e}")
202
+ ```
203
+
204
+ ### Cleaning up
205
+
206
+ ```python
207
+ from private_files import delete_private_dir
208
+
209
+ # Removes the directory and everything under it. No error if it doesn't exist.
210
+ delete_private_dir("sessions", app_name="myapp")
211
+ ```
212
+
213
+ ## Supported Python Versions
214
+
215
+ Python 3.10 through 3.14.
216
+
217
+ ## License
218
+
219
+ MIT. See [LICENSE](LICENSE).
220
+
221
+ ---
222
+
223
+ For development and release workflow documentation, see [CONTRIBUTING.md](CONTRIBUTING.md).