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.
- fs_schema-0.4.5/LICENSE +21 -0
- fs_schema-0.4.5/PKG-INFO +107 -0
- fs_schema-0.4.5/README.md +67 -0
- fs_schema-0.4.5/docs/index.md +1 -0
- fs_schema-0.4.5/docs/llms.txt +35 -0
- fs_schema-0.4.5/docs/reference.md +425 -0
- fs_schema-0.4.5/docs/run-help.txt +62 -0
- fs_schema-0.4.5/docs/stylesheets/extra.css +8 -0
- fs_schema-0.4.5/docs/tutorial.md +362 -0
- fs_schema-0.4.5/pyproject.toml +308 -0
- fs_schema-0.4.5/pyproject.toml.orig +222 -0
- fs_schema-0.4.5/src/fs_schema/__init__.py +31 -0
- fs_schema-0.4.5/src/fs_schema/_fmt.py +162 -0
- fs_schema-0.4.5/src/fs_schema/_mashumaro_json.py +70 -0
- fs_schema-0.4.5/src/fs_schema/_ops.py +66 -0
- fs_schema-0.4.5/src/fs_schema/_schema.py +981 -0
- fs_schema-0.4.5/src/fs_schema/_std_ext.py +35 -0
- fs_schema-0.4.5/src/fs_schema/_types.py +52 -0
- fs_schema-0.4.5/src/fs_schema/py.typed +0 -0
- fs_schema-0.4.5/tests/conftest.py +32 -0
- fs_schema-0.4.5/tests/test_docs.py +101 -0
- fs_schema-0.4.5/tests/test_examples.py +13 -0
- fs_schema-0.4.5/tests/test_fmt.py +132 -0
- fs_schema-0.4.5/tests/test_import.py +137 -0
- fs_schema-0.4.5/tests/test_mashumaro_json.py +141 -0
- fs_schema-0.4.5/tests/test_ops.py +97 -0
- fs_schema-0.4.5/tests/test_release.py +453 -0
- fs_schema-0.4.5/tests/test_schema_binding.py +366 -0
- fs_schema-0.4.5/tests/test_schema_declarations.py +63 -0
- fs_schema-0.4.5/tests/test_schema_planning.py +205 -0
- fs_schema-0.4.5/tests/test_schema_reification.py +407 -0
- fs_schema-0.4.5/tests/test_schema_runtime.py +226 -0
- fs_schema-0.4.5/tests/test_script_hooks.py +194 -0
- fs_schema-0.4.5/tests/test_std_ext.py +44 -0
- fs_schema-0.4.5/tests/typecheck/test_runtime_shapes.py +111 -0
- fs_schema-0.4.5/zensical.toml +44 -0
fs_schema-0.4.5/LICENSE
ADDED
|
@@ -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.
|
fs_schema-0.4.5/PKG-INFO
ADDED
|
@@ -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
|
+
```
|