fs-schema 0.4.5__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.
Files changed (36) hide show
  1. fs_schema-0.4.5/LICENSE +21 -0
  2. fs_schema-0.4.5/PKG-INFO +107 -0
  3. fs_schema-0.4.5/README.md +67 -0
  4. fs_schema-0.4.5/docs/index.md +1 -0
  5. fs_schema-0.4.5/docs/llms.txt +35 -0
  6. fs_schema-0.4.5/docs/reference.md +425 -0
  7. fs_schema-0.4.5/docs/run-help.txt +62 -0
  8. fs_schema-0.4.5/docs/stylesheets/extra.css +8 -0
  9. fs_schema-0.4.5/docs/tutorial.md +362 -0
  10. fs_schema-0.4.5/pyproject.toml +308 -0
  11. fs_schema-0.4.5/pyproject.toml.orig +222 -0
  12. fs_schema-0.4.5/src/fs_schema/__init__.py +31 -0
  13. fs_schema-0.4.5/src/fs_schema/_fmt.py +162 -0
  14. fs_schema-0.4.5/src/fs_schema/_mashumaro_json.py +70 -0
  15. fs_schema-0.4.5/src/fs_schema/_ops.py +66 -0
  16. fs_schema-0.4.5/src/fs_schema/_schema.py +981 -0
  17. fs_schema-0.4.5/src/fs_schema/_std_ext.py +35 -0
  18. fs_schema-0.4.5/src/fs_schema/_types.py +52 -0
  19. fs_schema-0.4.5/src/fs_schema/py.typed +0 -0
  20. fs_schema-0.4.5/tests/conftest.py +32 -0
  21. fs_schema-0.4.5/tests/test_docs.py +101 -0
  22. fs_schema-0.4.5/tests/test_examples.py +13 -0
  23. fs_schema-0.4.5/tests/test_fmt.py +132 -0
  24. fs_schema-0.4.5/tests/test_import.py +137 -0
  25. fs_schema-0.4.5/tests/test_mashumaro_json.py +141 -0
  26. fs_schema-0.4.5/tests/test_ops.py +97 -0
  27. fs_schema-0.4.5/tests/test_release.py +453 -0
  28. fs_schema-0.4.5/tests/test_schema_binding.py +366 -0
  29. fs_schema-0.4.5/tests/test_schema_declarations.py +63 -0
  30. fs_schema-0.4.5/tests/test_schema_planning.py +205 -0
  31. fs_schema-0.4.5/tests/test_schema_reification.py +407 -0
  32. fs_schema-0.4.5/tests/test_schema_runtime.py +226 -0
  33. fs_schema-0.4.5/tests/test_script_hooks.py +194 -0
  34. fs_schema-0.4.5/tests/test_std_ext.py +44 -0
  35. fs_schema-0.4.5/tests/typecheck/test_runtime_shapes.py +111 -0
  36. fs_schema-0.4.5/zensical.toml +44 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 HeyEntropyYield
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,107 @@
1
+ Metadata-Version: 2.4
2
+ Name: fs-schema
3
+ Version: 0.4.5
4
+ Summary: Strongly typed Python schemas for binding and validating filesystem layouts
5
+ Keywords: filesystem,schema,pathlib,typed,layout
6
+ Author: HeyEntropyYield
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Typing :: Typed
21
+ Requires-Dist: beartype>=0.23.0rc1
22
+ Requires-Dist: glom
23
+ Requires-Dist: parse
24
+ Requires-Dist: typing-extensions>=4.10
25
+ Requires-Dist: mashumaro>=3.22 ; extra == 'mashumaro'
26
+ Requires-Dist: mashumaro[msgpack]>=3.22 ; extra == 'msgpack'
27
+ Requires-Dist: mashumaro[orjson]>=3.22 ; extra == 'orjson'
28
+ Requires-Dist: mashumaro[toml]>=3.22 ; extra == 'toml'
29
+ Requires-Dist: mashumaro[yaml]>=3.22 ; extra == 'yaml'
30
+ Requires-Python: >=3.10
31
+ Project-URL: Homepage, https://github.com/HeyEntropyYield/fs-schema
32
+ Project-URL: Repository, https://github.com/HeyEntropyYield/fs-schema
33
+ Project-URL: Issues, https://github.com/HeyEntropyYield/fs-schema/issues
34
+ Provides-Extra: mashumaro
35
+ Provides-Extra: msgpack
36
+ Provides-Extra: orjson
37
+ Provides-Extra: toml
38
+ Provides-Extra: yaml
39
+ Description-Content-Type: text/markdown
40
+
41
+ # fs-schema
42
+
43
+ Typed schemas for filesystem layouts. Dataclass-like declarations turn directory
44
+ contracts into validated, navigable Python values.
45
+
46
+ ```bash
47
+ uv add "fs-schema[mashumaro,orjson]"
48
+ ```
49
+
50
+ ```python
51
+ from dataclasses import dataclass
52
+
53
+ import fs_schema as fss
54
+
55
+ @dataclass
56
+ class Contents:
57
+ title: str
58
+
59
+ class DataDownload(fss.Schema):
60
+ schema = {
61
+ "packs": {
62
+ fss.FILES: ["upload.log", "request.log"],
63
+ fss.Dir(alias="days", fmt="{day:%Y-%m-%d}"): {
64
+ "parts": fss.File(fmt="{stem}.{ext}", max=4),
65
+ },
66
+ },
67
+ "contents": fss.File("contents.json", schema=Contents),
68
+ }
69
+
70
+ download = fss.raise_mismatch(DataDownload.bind("."))
71
+ with open(download.packs.upload_log, encoding="utf-8") as stream:
72
+ print(stream.read())
73
+ part = download.packs.days[-1].parts[-1]
74
+ print(part.kwargs.stem, part.path.stat().st_size)
75
+ contents: Contents = fss.raise_exn(download.contents.load())
76
+ print(contents.title)
77
+ ```
78
+
79
+ Binding validates an existing layout. Templates are collections; indexing selects
80
+ a concrete match whose parsed captures are available through `.args` and
81
+ `.kwargs`. `relative_to()` plans output paths without claiming they exist.
82
+ Format-backed collections plan concrete files or recursively navigable
83
+ directories; only the top-level plan binds the whole schema. Dataclass schemas
84
+ use Mashumaro for JSON; install the `mashumaro` or faster `orjson` extra.
85
+
86
+ <details>
87
+ <summary>Expanded quickstart</summary>
88
+
89
+ ```python
90
+ --8<-- "examples/quickstart.py"
91
+ ```
92
+
93
+ </details>
94
+
95
+ <details>
96
+ <summary>Development</summary>
97
+
98
+ ```text
99
+ --8<-- "docs/run-help.txt"
100
+ ```
101
+
102
+ </details>
103
+
104
+ [API reference](https://heyentropyyield.github.io/fs-schema/reference/) ·
105
+ [Tutorial](https://heyentropyyield.github.io/fs-schema/tutorial/) ·
106
+ [Source](https://github.com/HeyEntropyYield/fs-schema) ·
107
+ [MIT LICENSE](https://heyentropyyield.github.io/fs-schema/LICENSE)
@@ -0,0 +1,67 @@
1
+ # fs-schema
2
+
3
+ Typed schemas for filesystem layouts. Dataclass-like declarations turn directory
4
+ contracts into validated, navigable Python values.
5
+
6
+ ```bash
7
+ uv add "fs-schema[mashumaro,orjson]"
8
+ ```
9
+
10
+ ```python
11
+ from dataclasses import dataclass
12
+
13
+ import fs_schema as fss
14
+
15
+ @dataclass
16
+ class Contents:
17
+ title: str
18
+
19
+ class DataDownload(fss.Schema):
20
+ schema = {
21
+ "packs": {
22
+ fss.FILES: ["upload.log", "request.log"],
23
+ fss.Dir(alias="days", fmt="{day:%Y-%m-%d}"): {
24
+ "parts": fss.File(fmt="{stem}.{ext}", max=4),
25
+ },
26
+ },
27
+ "contents": fss.File("contents.json", schema=Contents),
28
+ }
29
+
30
+ download = fss.raise_mismatch(DataDownload.bind("."))
31
+ with open(download.packs.upload_log, encoding="utf-8") as stream:
32
+ print(stream.read())
33
+ part = download.packs.days[-1].parts[-1]
34
+ print(part.kwargs.stem, part.path.stat().st_size)
35
+ contents: Contents = fss.raise_exn(download.contents.load())
36
+ print(contents.title)
37
+ ```
38
+
39
+ Binding validates an existing layout. Templates are collections; indexing selects
40
+ a concrete match whose parsed captures are available through `.args` and
41
+ `.kwargs`. `relative_to()` plans output paths without claiming they exist.
42
+ Format-backed collections plan concrete files or recursively navigable
43
+ directories; only the top-level plan binds the whole schema. Dataclass schemas
44
+ use Mashumaro for JSON; install the `mashumaro` or faster `orjson` extra.
45
+
46
+ <details>
47
+ <summary>Expanded quickstart</summary>
48
+
49
+ ```python
50
+ --8<-- "examples/quickstart.py"
51
+ ```
52
+
53
+ </details>
54
+
55
+ <details>
56
+ <summary>Development</summary>
57
+
58
+ ```text
59
+ --8<-- "docs/run-help.txt"
60
+ ```
61
+
62
+ </details>
63
+
64
+ [API reference](https://heyentropyyield.github.io/fs-schema/reference/) ·
65
+ [Tutorial](https://heyentropyyield.github.io/fs-schema/tutorial/) ·
66
+ [Source](https://github.com/HeyEntropyYield/fs-schema) ·
67
+ [MIT LICENSE](https://heyentropyyield.github.io/fs-schema/LICENSE)
@@ -0,0 +1 @@
1
+ --8<-- "README.md"
@@ -0,0 +1,35 @@
1
+ # fs-schema
2
+
3
+ > Python 3.10+ typed schemas for filesystem layouts. Declare a tree, bind and
4
+ > validate an existing directory, or root a tree before writing it.
5
+
6
+ Import as `import fs_schema as fss`.
7
+
8
+ Core state model:
9
+
10
+ - `Schema` subclass: layout declaration and validated bound value.
11
+ - `Schema.bind(path)`: validate existing tree; returns schema or `MismatchErr`.
12
+ - `Schema.relative_to(path)`: planned paths; returns `SchemaRoot[Schema]`.
13
+ - `SchemaRoot.bind()`: validate written tree and return schema.
14
+ - `File`, `Dir`, `FILES`: layout declaration.
15
+ - Attribute/index access: generated, schema-aware traversal.
16
+ - `fmt`, `match`, `min`, `max`, `sort`: repeated-node matching.
17
+ - `Match.args`, `Match.kwargs`, `filter`, `find`, `where`, `get`: collection queries and captured-value access.
18
+ - `put`, `load`, `raise_exn`, `path`: filesystem I/O.
19
+ - Optional Mashumaro integration: dataclass JSON, with an optional `orjson` backend.
20
+
21
+ `Schema` values are validated filesystem states. `SchemaRoot[T]` values are
22
+ rooted write destinations not yet validated.
23
+
24
+ ## Documentation
25
+
26
+ - [API reference Markdown](https://raw.githubusercontent.com/HeyEntropyYield/fs-schema/master/docs/reference.md): Declaration, binding, rooted creation, node access, templates, I/O, and errors.
27
+ - [Tutorial Markdown](https://raw.githubusercontent.com/HeyEntropyYield/fs-schema/master/docs/tutorial.md): Data-delivery pipeline before and after filesystem types.
28
+
29
+ ## Code
30
+
31
+ - [Quickstart](https://github.com/HeyEntropyYield/fs-schema/blob/master/examples/quickstart.py): Compact declaration, binding, access, output formatting, and writing.
32
+ - [Before fs-schema](https://github.com/HeyEntropyYield/fs-schema/blob/master/examples/data_delivery_before.py): Path-only pipeline with layout contract in prose.
33
+ - [After fs-schema](https://github.com/HeyEntropyYield/fs-schema/blob/master/examples/data_delivery_after.py): Same pipeline with filesystem state types.
34
+ - [Public API source](https://github.com/HeyEntropyYield/fs-schema/blob/master/src/fs_schema/__init__.py): Explicit public API facade.
35
+ - [Repository](https://github.com/HeyEntropyYield/fs-schema): Package source, issues, and development files.
@@ -0,0 +1,425 @@
1
+ # API reference
2
+
3
+ Examples on this page build on these imports and definitions:
4
+
5
+ ```python
6
+ from dataclasses import dataclass
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+
10
+ import fs_schema as fss
11
+ ```
12
+
13
+ ## Public API at a glance
14
+
15
+ | Area | Names |
16
+ | --- | --- |
17
+ | Schemas | `Schema`, `SchemaRoot`, `Layout` |
18
+ | Declarations | `File`, `Dir`, `FILES`, `dt` |
19
+ | Paths and matches | `Located`, `Match`, `exists_opt` |
20
+ | Result helpers | `MismatchErr`, `is_mismatch`, `raise_mismatch`, `raise_exn` |
21
+ | Writing | `put` |
22
+ | Package metadata | `__version__` |
23
+
24
+ All names in this table are available from `fs_schema`.
25
+
26
+ ## Defining schemas
27
+
28
+ Subclass `Schema` and define a `schema` mapping. Nested mappings represent
29
+ nested directories. A Schema subclass has exactly one direct Schema base.
30
+ Schema subclasses are layout declarations; do not add methods, mixins, custom
31
+ metaclasses, or other behavior, as those uses are unsupported.
32
+
33
+ ```python
34
+ @dataclass
35
+ class Manifest:
36
+ delivery_id: str
37
+ rows: int
38
+
39
+
40
+ class Batch(fss.Schema):
41
+ schema = {
42
+ "parts": fss.File(fmt="part-{part:d}.parquet"),
43
+ }
44
+
45
+
46
+ class Delivery(fss.Schema):
47
+ schema = {
48
+ "transfer": {
49
+ fss.FILES: ["download.log", "request.json"],
50
+ },
51
+ "manifest": fss.File("manifest.json", schema=Manifest),
52
+ "batches": {
53
+ fss.Dir(
54
+ alias="days",
55
+ fmt="{day:%Y-%m-%d}",
56
+ sort=lambda match: match.kwargs["day"],
57
+ ): Batch,
58
+ },
59
+ }
60
+ ```
61
+
62
+ ### Mapping forms
63
+
64
+ | Entry | Declares |
65
+ | --- | --- |
66
+ | `"dir": {...}` or `"dir": ChildSchema` | A fixed directory |
67
+ | `Dir(...): {...}` or `Dir(...): ChildSchema` | A configured directory |
68
+ | `"alias": "file.ext"` or `"alias": File(...)` | One file |
69
+ | `FILES: ["file.ext", File(...)]` | Files in the current directory |
70
+
71
+ Only `name` is positional in `File` and `Dir`. All other options are
72
+ keyword-only. Every declaration requires `name`, `fmt`, or `match`. For a fixed
73
+ directory, an explicit `ChildSchema` is both the nested layout and its exact
74
+ runtime type. A fixed inline mapping receives one private `Schema` subtype with
75
+ stable identity, visible through recursive class-level navigation.
76
+
77
+ `Dir(..., schema=Required)` checks the right-hand-side layout when the
78
+ containing Schema is defined. It must contain the required declarations
79
+ recursively; extra declarations are fine. `Required` is not merged, and the
80
+ right-hand side still controls the directory's children and runtime type.
81
+
82
+ ### Templates, matching, and allowed match counts
83
+
84
+ | Option | Meaning |
85
+ | --- | --- |
86
+ | `name` | Exact basename. It cannot be combined with `fmt`. |
87
+ | `fmt` | Full-basename parse and format template, validated when declared. |
88
+ | `match` | Regex selector on full basename with `re.fullmatch`, compiled when declared. |
89
+ | `min` | Minimum count; defaults to `1`. Use `0` for an optional `fmt`/`match` collection. |
90
+ | `max` | Maximum count. Exact names require `min=max=1`; templates are unbounded. |
91
+ | `alias` | Name used for child access in Python. |
92
+ | `sort` | Key function for template matches. |
93
+ | `sort_rev` | Reverse the match order when true. |
94
+ | `schema` | Loader for a file, or an extra schema contract for a directory. |
95
+
96
+ With `name`, `match` validates the exact basename. With `fmt`, it adds a filter without changing format captures. Used alone, `match` exposes unnamed groups in `args` and named groups in `kwargs`. Optional groups produce `None`.
97
+
98
+ Untyped format fields produce `str`, `:d` fields produce `int`, and datetime format fields produce `datetime`.
99
+
100
+ ```python
101
+ part_decl = fss.File(
102
+ fmt="part-{part:d}.{ext}",
103
+ match=r"(?i).+\.(?:json|yaml|toml)",
104
+ )
105
+ day_decl = fss.Dir(alias="days", fmt=fss.dt("%Y-%m-%d"), min=0)
106
+ ```
107
+
108
+ `fss.dt("%Y-%m-%d")` returns `"{:%Y-%m-%d}"`. Its datetime is capture
109
+ `args[0]`. A named field such as `{day:%Y-%m-%d}` is capture `kwargs["day"]`.
110
+
111
+ ### Inheritance and replacement
112
+
113
+ Ordinary inheritance starts with the direct base's effective layout. The
114
+ subclass may add declarations or replace inherited ones.
115
+
116
+ ```python
117
+ class AuditedDelivery(Delivery):
118
+ schema = {
119
+ "audit": "audit.json",
120
+ }
121
+ ```
122
+
123
+ Within one directory, distinct children cannot collide by alias, exact name,
124
+ normalized name, or `fmt`; ambiguous layouts fail when the class is defined.
125
+ Aliases, names, and formats identify inherited declarations for whole-node
126
+ replacement. Match-only declarations without one of those identities append.
127
+
128
+ ### Declaration API
129
+
130
+ ```text
131
+ File(
132
+ name: str = "",
133
+ *,
134
+ fmt: FmtLike | None = None,
135
+ match: str | None = None,
136
+ min: int = 1,
137
+ max: int | None = None,
138
+ alias: str | None = None,
139
+ sort: Callable[[Match], str | int | float | datetime | Located] | None = None,
140
+ sort_rev: bool = False,
141
+ schema: type[T] | Callable[[Path], T] | None = None,
142
+ ) -> File[T]
143
+
144
+ Dir(
145
+ name: str = "",
146
+ *,
147
+ fmt: FmtLike | None = None,
148
+ match: str | None = None,
149
+ min: int = 1,
150
+ max: int | None = None,
151
+ alias: str | None = None,
152
+ sort: Callable[[Match], str | int | float | datetime | Located] | None = None,
153
+ sort_rev: bool = False,
154
+ schema: type[S] | None = None,
155
+ ) -> Dir[S]
156
+
157
+ dt(pattern: str) -> FmtLike
158
+ ```
159
+
160
+ The mapping table above defines the valid `Layout` key-value pairings.
161
+
162
+ ## Applying schemas
163
+
164
+ `bind` checks an existing directory tree. It returns an ordinary instance of the
165
+ exact requested schema class on success (`type(result) is Delivery` below), or a
166
+ `MismatchErr` value on failure. Fixed directories backed by explicit or inline
167
+ schemas have that exact Schema runtime type. Repeated directories bind to
168
+ collections whose elements are ordinary directory matches with captures and
169
+ child navigation.
170
+
171
+ ```python
172
+ result = Delivery.bind("/srv/incoming/delivery-42")
173
+ if fss.is_mismatch(result):
174
+ print(result)
175
+ else:
176
+ delivery: Delivery = result
177
+ ```
178
+
179
+ Use `raise_mismatch` when a mismatch should be raised:
180
+
181
+ ```python
182
+ delivery = fss.raise_mismatch(
183
+ Delivery.bind("/srv/incoming/delivery-42")
184
+ )
185
+ ```
186
+
187
+ `Schema.bind` also accepts a `Located` value. A planned root binds itself.
188
+
189
+ ```python
190
+ validated_again = Delivery.bind(delivery)
191
+ planned_delivery = Delivery.relative_to("/srv/incoming/delivery-42")
192
+ validated_from_plan = planned_delivery.bind()
193
+ ```
194
+
195
+ Binding checks declared structure and allowed match counts at that moment. It
196
+ ignores undeclared entries, is not a filesystem lock, and returns the first
197
+ mismatch.
198
+
199
+ ```text
200
+ Schema.bind(root: str | os.PathLike[str] | Located) -> Self | MismatchErr
201
+ SchemaRoot[S].bind() -> S | MismatchErr
202
+ is_mismatch(x: object) -> TypeIs[MismatchErr]
203
+ raise_mismatch(x: T | MismatchErr) -> T
204
+ ```
205
+
206
+ ## Using schemas
207
+
208
+ ### Fixed directories and files
209
+
210
+ A bound `Schema`, planned `SchemaRoot`, fixed directory, or fixed file
211
+ represents one path and satisfies `Located`. Each works with `os.fspath` and
212
+ APIs that accept `os.PathLike[str]`.
213
+
214
+ ```python
215
+ root_path = delivery.path
216
+ manifest_path = Path(delivery.manifest)
217
+ request_text = delivery.transfer.request_json.read_text()
218
+ request_exists = delivery.transfer.request_json.exists()
219
+ ```
220
+
221
+ `Located` promises only `.path` and `__fspath__`. Concrete fixed values also
222
+ have `exists()`. Fixed files add `read_bytes()`, `read_text()`, and `put()`.
223
+
224
+ Children support attribute access and exact item lookup. An explicit alias is
225
+ used unchanged. Otherwise, each run outside `[A-Za-z0-9_]` in the disk name
226
+ becomes `_`.
227
+
228
+ ```python
229
+ log_file = delivery.transfer.download_log
230
+ same_request = delivery.transfer["request.json"]
231
+ ```
232
+
233
+ Use `exists_opt(path)` when an absent optional node should become `None` rather
234
+ than a missing path:
235
+
236
+ ```text
237
+ exists_opt(path: str | os.PathLike[str]) -> Path | None
238
+ ```
239
+
240
+ ### Template collections and matches
241
+
242
+ A template declaration provides a sequence of its current matches. The sequence
243
+ has the parent directory in `.path`, but it is not `Located`. Indexing or
244
+ iteration returns a concrete `Match`, which is `Located` and contains parsed
245
+ captures.
246
+
247
+ ```python
248
+ days = delivery.batches.days
249
+ parent_directory = days.path
250
+ latest_day = days[-1]
251
+ day_value = latest_day.kwargs["day"]
252
+
253
+ for day in days:
254
+ for part in day.parts:
255
+ part_number = part.kwargs.part
256
+ part_path = part.path
257
+ ```
258
+
259
+ `args` contains unnamed captures. `kwargs` supports mapping and attribute
260
+ access for named captures. Optional regex captures can be `None`. Use mapping
261
+ lookup for names such as `"items"` that collide with mapping methods.
262
+
263
+ | Collection operation | Result |
264
+ | --- | --- |
265
+ | `collection[index]` | One `Match`; normal sequence indexing may raise `IndexError` |
266
+ | `collection[slice]` | Another collection of the same concrete type |
267
+ | `filter(predicate)` | Accepted matches as the same concrete collection type, in collection order |
268
+ | `find(predicate)` | The first accepted match, or `None` |
269
+ | `where(*args, **kwargs)` | Matches whose captures have the positional prefix and named subset |
270
+ | `get(index=0, default=...)` | Safely indexed match, or the supplied default when out of range |
271
+ | `format(*args, **kwargs)` | Planned fixed file or recursively navigable fixed directory (format-backed collections only) |
272
+
273
+ A predicate receives the pair `(match.args, match.kwargs)`. Filtering returns a
274
+ materialized result that keeps collection capabilities such as slicing and, for
275
+ format-backed collections, `format()`. Empty results are collections too.
276
+
277
+ `where` combines all supplied constraints. Positional values match a capture
278
+ prefix, with `None` acting as a wildcard for that position; named values match a
279
+ subset, where `None` matches an optional capture value. Unknown named capture
280
+ fields raise `KeyError`, even on an empty planned collection. More positional
281
+ values than a match has simply reject that match. Calling `where()` without
282
+ constraints returns an equivalent collection.
283
+
284
+ `get` accepts positive and negative indexes. With no explicit default, an
285
+ out-of-range index returns `None`; an explicit `None` or other default is
286
+ returned unchanged. Other errors are not hidden. This makes one-result queries
287
+ concise without changing ordinary sequence indexing.
288
+
289
+ ```python
290
+ def is_selected_day(args, kwargs):
291
+ return kwargs["day"] == datetime(2026, 9, 10)
292
+
293
+ selected_days = days.filter(is_selected_day)
294
+ selected_day = days.find(is_selected_day)
295
+ selected_by_capture = days.where(day=datetime(2026, 9, 10))
296
+ selected_by_capture_or_none = selected_by_capture.get()
297
+ ```
298
+
299
+ ```text
300
+ filter(predicate, /) -> Self
301
+ find(predicate, /) -> Match | None
302
+ where(*args: str | int | datetime | None,
303
+ **kwargs: str | int | datetime | None) -> Self
304
+ get(index: int = 0) -> Match | None
305
+ get(index: int, default: T) -> Match | T
306
+ get(*, default: T) -> Match | T
307
+ ```
308
+
309
+ Capture names are dynamic, so static checking does not try to narrow keyword
310
+ names passed to `where`.
311
+
312
+ Formatting a format-backed collection plans one concrete path without I/O or
313
+ captures. A formatted directory remains recursively navigable, and a formatted
314
+ file retains its read, load, and put methods. Regex-only collections and
315
+ individual matches are not formattable.
316
+
317
+ ```python
318
+ planned_day = days.format(day=datetime(2026, 9, 11))
319
+ ```
320
+
321
+ ### Public path and match protocols
322
+
323
+ ```text
324
+ @runtime_checkable
325
+ class Located(Protocol):
326
+ @property
327
+ def path(self) -> Path: ...
328
+ def __fspath__(self) -> str: ...
329
+
330
+ @runtime_checkable
331
+ class Match(Located, Protocol):
332
+ @property
333
+ def args(self) -> tuple[str | int | datetime | None, ...]: ...
334
+ @property
335
+ def kwargs(self) -> Mapping[str, str | int | datetime | None]: ...
336
+ ```
337
+
338
+ The concrete `kwargs` mapping also supports capture access by attribute, as
339
+ shown above.
340
+
341
+ ### Static typing of dynamic children
342
+
343
+ Dynamically resolved class children use the bare `type` boundary. This is broad
344
+ enough for direct recursive navigation such as `Root.inline.leaf`, but it cannot
345
+ preserve whether each dynamically named child is a file class, collection class,
346
+ or exact generated `Schema` subtype. Instance child access likewise returns a
347
+ union of possible child kinds. `Delivery.RootT` remains
348
+ `type[SchemaRoot[Schema]]`.
349
+
350
+ Use an explicit `fss.SchemaRoot[Delivery]` annotation when the schema parameter
351
+ must remain exact. Fixed Schema-backed values retain their exact classes; only
352
+ dynamic static lookup loses that precision.
353
+
354
+ ## Creating with schemas
355
+
356
+ `relative_to` creates a planned layout without checking the filesystem. Fixed
357
+ children already have paths. Planned collections are empty until binding; a
358
+ format-backed collection can produce one concrete planned child, while a
359
+ regex-only collection cannot be concretized without matching the filesystem.
360
+ Writing and validation remain separate.
361
+
362
+ ```python
363
+ planned: fss.SchemaRoot[Delivery] = Delivery.relative_to(
364
+ "/srv/curated/delivery-42"
365
+ )
366
+ event_date = datetime(2026, 9, 10)
367
+ planned_day = planned.batches.days.format(day=event_date)
368
+ planned_part = planned_day.parts.format(part=0)
369
+
370
+ manifest = Manifest("delivery-42", 1_000)
371
+ parquet_bytes = b"parquet payload"
372
+ planned.manifest.put(manifest)
373
+ planned_part.put(parquet_bytes)
374
+
375
+ created = planned.bind()
376
+ ```
377
+
378
+ `SchemaRoot[Delivery]` keeps the schema parameter, so `bind()` returns
379
+ `Delivery | MismatchErr`. Calling `root()` on a bound schema creates a plan at
380
+ the same path and intentionally drops the validation guarantee.
381
+
382
+ Every descendant of a rooted plan is also planned and derived from the same
383
+ canonical declarations. Only the top-level `SchemaRoot` retains the schema/root
384
+ token and exposes `bind()`; descendants do not independently bind or return to
385
+ the root. Keep the root plan when that transition is needed.
386
+
387
+ ```python
388
+ reopened_plan: fss.SchemaRoot[Delivery] = delivery.root()
389
+ ```
390
+
391
+ ### Reading and writing
392
+
393
+ | Operation | Result |
394
+ | --- | --- |
395
+ | `file.read_bytes()` | `bytes` |
396
+ | `file.read_text()` | `str` |
397
+ | `file.put(data)` | Writes to the declared file |
398
+ | `file.load()` | Declared value or decoding exception |
399
+ | `fss.put(path, data)` | Lower-level direct-path helper; creates missing parent directories |
400
+
401
+ ```python
402
+ manifest: Manifest = fss.raise_exn(delivery.manifest.load())
403
+
404
+
405
+ def load_manifest(path: Path) -> Manifest:
406
+ return Manifest(path.stem, 0)
407
+
408
+
409
+ custom_manifest = fss.File(
410
+ "manifest.custom",
411
+ schema=load_manifest,
412
+ )
413
+ ```
414
+
415
+ `put` creates the target parent, then writes bytes or text, copies a source
416
+ `Path`, or calls `save(Path)`. Other dataclass instances are encoded as JSON
417
+ with Mashumaro. Install `fs-schema[mashumaro]` for the standard backend or
418
+ `fs-schema[orjson]` to prefer its faster backend.
419
+ `load()` returns decoding failures as values, and `raise_exn` raises one while
420
+ preserving the successful result type.
421
+
422
+ ```text
423
+ put(path, data) -> None
424
+ raise_exn(value: T | Exception) -> T
425
+ ```