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.
- private_files-1.0.0/LICENSE +21 -0
- private_files-1.0.0/PKG-INFO +273 -0
- private_files-1.0.0/README.md +223 -0
- private_files-1.0.0/private_files/__init__.py +514 -0
- private_files-1.0.0/private_files/py.typed +0 -0
- private_files-1.0.0/pyproject.toml +111 -0
- private_files-1.0.0/tests/test_private_files.py +335 -0
|
@@ -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
|
+
[](https://github.com/mckelvie-org/private-files/actions/workflows/ci.yml)
|
|
54
|
+
[](https://pypi.org/project/private-files/1.0.0/)
|
|
55
|
+
[](https://pypi.org/project/private-files/)
|
|
56
|
+
[](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 — API tokens, session cookies,
|
|
61
|
+
credentials, profile data — 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 — 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()` — including `@overload`-based mode-based return-type inference (`TextIO` vs
|
|
86
|
+
`BinaryIO`) — 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 — 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 — 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) — 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
|
+
[](https://github.com/mckelvie-org/private-files/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/private-files/1.0.0/)
|
|
5
|
+
[](https://pypi.org/project/private-files/)
|
|
6
|
+
[](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 — API tokens, session cookies,
|
|
11
|
+
credentials, profile data — 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 — 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()` — including `@overload`-based mode-based return-type inference (`TextIO` vs
|
|
36
|
+
`BinaryIO`) — 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 — 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 — 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) — 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).
|