wexample-file 0.0.1__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.
- wexample_file-0.0.1/LICENSE +21 -0
- wexample_file-0.0.1/PKG-INFO +38 -0
- wexample_file-0.0.1/README.md +17 -0
- wexample_file-0.0.1/pyproject.toml +43 -0
- wexample_file-0.0.1/setup.cfg +4 -0
- wexample_file-0.0.1/tests/__init__.py +0 -0
- wexample_file-0.0.1/tests/package/common/test_local_directory.py +76 -0
- wexample_file-0.0.1/tests/package/common/test_local_file.py +134 -0
- wexample_file-0.0.1/wexample_file/__init__.py +0 -0
- wexample_file-0.0.1/wexample_file/common/abstract_local_item_path.py +85 -0
- wexample_file-0.0.1/wexample_file/common/local_directory.py +54 -0
- wexample_file-0.0.1/wexample_file/common/local_file.py +95 -0
- wexample_file-0.0.1/wexample_file/const/globals.py +2 -0
- wexample_file-0.0.1/wexample_file/const/types.py +4 -0
- wexample_file-0.0.1/wexample_file/excpetion/directory_not_found_exception.py +9 -0
- wexample_file-0.0.1/wexample_file/excpetion/file_not_found_exception.py +9 -0
- wexample_file-0.0.1/wexample_file/excpetion/local_path_not_found_exception.py +9 -0
- wexample_file-0.0.1/wexample_file/excpetion/not_a_directory_exception.py +9 -0
- wexample_file-0.0.1/wexample_file/excpetion/not_a_file_exception.py +9 -0
- wexample_file-0.0.1/wexample_file.egg-info/PKG-INFO +38 -0
- wexample_file-0.0.1/wexample_file.egg-info/SOURCES.txt +22 -0
- wexample_file-0.0.1/wexample_file.egg-info/dependency_links.txt +1 -0
- wexample_file-0.0.1/wexample_file.egg-info/requires.txt +6 -0
- wexample_file-0.0.1/wexample_file.egg-info/top_level.txt +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) [year] [fullname]
|
|
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,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wexample-file
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Package that allows you to manage the state of files and directories using YAML configuration files.
|
|
5
|
+
Author-email: weeger <contact@wexample.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: homepage, https://github.com/wexample/python-file
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.6
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: pip-tools
|
|
15
|
+
Requires-Dist: pydantic
|
|
16
|
+
Requires-Dist: pytest
|
|
17
|
+
Requires-Dist: wexample-config==0.0.41
|
|
18
|
+
Requires-Dist: wexample-helpers==0.0.57
|
|
19
|
+
Requires-Dist: wexample-prompt==0.0.38
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# wexample-file
|
|
23
|
+
|
|
24
|
+
Tools to manage the state of files and directories using simple YAML configuration files.
|
|
25
|
+
|
|
26
|
+
- Project: https://github.com/wexample/python-file
|
|
27
|
+
- License: MIT
|
|
28
|
+
|
|
29
|
+
Install:
|
|
30
|
+
```bash
|
|
31
|
+
pip install wexample-file
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Quick start:
|
|
35
|
+
```python
|
|
36
|
+
from wexample_file import *
|
|
37
|
+
# TODO: usage examples will go here
|
|
38
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# wexample-file
|
|
2
|
+
|
|
3
|
+
Tools to manage the state of files and directories using simple YAML configuration files.
|
|
4
|
+
|
|
5
|
+
- Project: https://github.com/wexample/python-file
|
|
6
|
+
- License: MIT
|
|
7
|
+
|
|
8
|
+
Install:
|
|
9
|
+
```bash
|
|
10
|
+
pip install wexample-file
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Quick start:
|
|
14
|
+
```python
|
|
15
|
+
from wexample_file import *
|
|
16
|
+
# TODO: usage examples will go here
|
|
17
|
+
```
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [
|
|
3
|
+
"setuptools",
|
|
4
|
+
"wheel",
|
|
5
|
+
]
|
|
6
|
+
build-backend = "setuptools.build_meta"
|
|
7
|
+
|
|
8
|
+
[project]
|
|
9
|
+
name = "wexample-file"
|
|
10
|
+
version = "0.0.1"
|
|
11
|
+
description = "Package that allows you to manage the state of files and directories using YAML configuration files."
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "weeger", email = "contact@wexample.com" },
|
|
14
|
+
]
|
|
15
|
+
requires-python = ">=3.6"
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"pip-tools",
|
|
23
|
+
"pydantic",
|
|
24
|
+
"pytest",
|
|
25
|
+
"wexample-config==0.0.41",
|
|
26
|
+
"wexample-helpers==0.0.57",
|
|
27
|
+
"wexample-prompt==0.0.38",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.readme]
|
|
31
|
+
file = "README.md"
|
|
32
|
+
content-type = "text/markdown"
|
|
33
|
+
|
|
34
|
+
[project.license]
|
|
35
|
+
text = "MIT"
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
homepage = "https://github.com/wexample/python-file"
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.packages.find]
|
|
41
|
+
include = [
|
|
42
|
+
"*",
|
|
43
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from wexample_file.common.local_directory import LocalDirectory
|
|
5
|
+
from wexample_file.excpetion.not_a_directory_exception import NotADirectoryException
|
|
6
|
+
from wexample_file.excpetion.directory_not_found_exception import DirectoryNotFoundException
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_local_directory_instantiation_with_str(tmp_path):
|
|
10
|
+
d = tmp_path / "adir"
|
|
11
|
+
d.mkdir()
|
|
12
|
+
ld = LocalDirectory(path=str(d))
|
|
13
|
+
assert isinstance(ld.path, Path)
|
|
14
|
+
assert ld.path == d.resolve()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_local_directory_instantiation_with_path_nonexistent(tmp_path):
|
|
18
|
+
d = tmp_path / "missing_dir"
|
|
19
|
+
assert not d.exists()
|
|
20
|
+
ld = LocalDirectory(path=d)
|
|
21
|
+
assert ld.path == d.resolve()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_local_directory_rejects_file(tmp_path):
|
|
25
|
+
f = tmp_path / "afile.txt"
|
|
26
|
+
f.write_text("hello")
|
|
27
|
+
with pytest.raises(NotADirectoryException):
|
|
28
|
+
LocalDirectory(path=f)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_local_directory_check_exists_true_accepts_existing_dir(tmp_path):
|
|
32
|
+
d = tmp_path / "exists_dir"
|
|
33
|
+
d.mkdir()
|
|
34
|
+
ld = LocalDirectory(path=d, check_exists=True)
|
|
35
|
+
assert ld.path == d.resolve()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_local_directory_check_exists_true_rejects_missing(tmp_path):
|
|
39
|
+
d = tmp_path / "missing_dir2"
|
|
40
|
+
assert not d.exists()
|
|
41
|
+
with pytest.raises(DirectoryNotFoundException):
|
|
42
|
+
LocalDirectory(path=d, check_exists=True)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_local_directory_remove_deletes_directory_recursively(tmp_path):
|
|
46
|
+
d = tmp_path / "adir_to_remove"
|
|
47
|
+
sub = d / "sub"
|
|
48
|
+
sub.mkdir(parents=True)
|
|
49
|
+
(sub / "file.txt").write_text("hello")
|
|
50
|
+
ld = LocalDirectory(path=d, check_exists=True)
|
|
51
|
+
assert d.exists() and d.is_dir()
|
|
52
|
+
ld.remove()
|
|
53
|
+
assert not d.exists()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_local_directory_remove_idempotent(tmp_path):
|
|
57
|
+
d = tmp_path / "missing_dir_after_remove"
|
|
58
|
+
ld = LocalDirectory(path=d)
|
|
59
|
+
# First remove on non-existent path should not raise
|
|
60
|
+
ld.remove()
|
|
61
|
+
assert not d.exists()
|
|
62
|
+
# Create then remove, then remove again
|
|
63
|
+
d.mkdir()
|
|
64
|
+
ld2 = LocalDirectory(path=d, check_exists=True)
|
|
65
|
+
ld2.remove()
|
|
66
|
+
assert not d.exists()
|
|
67
|
+
# Idempotent second call
|
|
68
|
+
ld2.remove()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_local_directory_create_creates_directory_and_parents(tmp_path):
|
|
72
|
+
d = tmp_path / "a/b/c"
|
|
73
|
+
ld = LocalDirectory(path=d)
|
|
74
|
+
assert not d.exists()
|
|
75
|
+
ld.create()
|
|
76
|
+
assert d.exists() and d.is_dir()
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from wexample_file.common.local_file import LocalFile
|
|
4
|
+
from wexample_file.excpetion.not_a_file_exception import NotAFileException
|
|
5
|
+
from wexample_file.excpetion.file_not_found_exception import FileNotFoundException
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_local_file_instantiation_with_str(tmp_path):
|
|
9
|
+
p = tmp_path / "file.txt"
|
|
10
|
+
p.write_text("hello")
|
|
11
|
+
lf = LocalFile(path=str(p))
|
|
12
|
+
assert isinstance(lf.path, Path)
|
|
13
|
+
assert lf.path == p.resolve()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_local_file_instantiation_with_path_nonexistent(tmp_path):
|
|
17
|
+
p = tmp_path / "missing.txt"
|
|
18
|
+
assert not p.exists()
|
|
19
|
+
lf = LocalFile(path=p)
|
|
20
|
+
assert lf.path == p.resolve()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_local_file_rejects_directory(tmp_path):
|
|
24
|
+
d = tmp_path / "adir"
|
|
25
|
+
d.mkdir()
|
|
26
|
+
with pytest.raises(NotAFileException):
|
|
27
|
+
LocalFile(path=d)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_local_file_check_exists_true_accepts_existing_file(tmp_path):
|
|
31
|
+
p = tmp_path / "exists.txt"
|
|
32
|
+
p.write_text("data")
|
|
33
|
+
lf = LocalFile(path=p, check_exists=True)
|
|
34
|
+
assert lf.path == p.resolve()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_local_file_check_exists_true_rejects_missing(tmp_path):
|
|
38
|
+
p = tmp_path / "missing2.txt"
|
|
39
|
+
assert not p.exists()
|
|
40
|
+
with pytest.raises(FileNotFoundException):
|
|
41
|
+
LocalFile(path=p, check_exists=True)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_local_file_remove_deletes_file(tmp_path):
|
|
45
|
+
p = tmp_path / "toremove.txt"
|
|
46
|
+
p.write_text("data")
|
|
47
|
+
lf = LocalFile(path=p, check_exists=True)
|
|
48
|
+
assert p.exists()
|
|
49
|
+
lf.remove()
|
|
50
|
+
assert not p.exists()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_local_file_remove_idempotent(tmp_path):
|
|
54
|
+
p = tmp_path / "missing_after_remove.txt"
|
|
55
|
+
lf = LocalFile(path=p)
|
|
56
|
+
# First remove on non-existent path should not raise
|
|
57
|
+
lf.remove()
|
|
58
|
+
assert not p.exists()
|
|
59
|
+
# Create then remove, then remove again
|
|
60
|
+
p.write_text("hello")
|
|
61
|
+
lf2 = LocalFile(path=p, check_exists=True)
|
|
62
|
+
lf2.remove()
|
|
63
|
+
assert not p.exists()
|
|
64
|
+
# Idempotent second call
|
|
65
|
+
lf2.remove()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_local_file_read_returns_content_when_exists(tmp_path):
|
|
69
|
+
p = tmp_path / "readme.txt"
|
|
70
|
+
content = "héllo world"
|
|
71
|
+
p.write_text(content, encoding="utf-8")
|
|
72
|
+
lf = LocalFile(path=p)
|
|
73
|
+
assert lf.read() == content
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_local_file_read_returns_none_when_missing(tmp_path):
|
|
77
|
+
p = tmp_path / "missing_read.txt"
|
|
78
|
+
lf = LocalFile(path=p)
|
|
79
|
+
assert lf.read() is None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_local_file_read_raises_if_not_a_file(tmp_path):
|
|
83
|
+
d = tmp_path / "not_a_file"
|
|
84
|
+
d.mkdir()
|
|
85
|
+
# Constructing LocalFile with a directory path would already raise NotAFileException
|
|
86
|
+
# So we simulate a race: create a file path then replace with directory
|
|
87
|
+
p = tmp_path / "was_file.txt"
|
|
88
|
+
p.write_text("x")
|
|
89
|
+
lf = LocalFile(path=p)
|
|
90
|
+
# Replace the path with a directory at same location
|
|
91
|
+
p.unlink()
|
|
92
|
+
d.rename(p)
|
|
93
|
+
|
|
94
|
+
assert lf.read() is None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_local_file_touch_creates_file_and_parents(tmp_path):
|
|
98
|
+
nested = tmp_path / "a/b/c/file.txt"
|
|
99
|
+
lf = LocalFile(path=nested)
|
|
100
|
+
assert not nested.exists()
|
|
101
|
+
lf.touch()
|
|
102
|
+
assert nested.exists() and nested.is_file()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_local_file_write_writes_content_and_creates_parents(tmp_path):
|
|
106
|
+
nested = tmp_path / "x/y/z/out.txt"
|
|
107
|
+
lf = LocalFile(path=nested)
|
|
108
|
+
text = "some content"
|
|
109
|
+
lf.write(text)
|
|
110
|
+
assert nested.exists() and nested.read_text() == text
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_local_file_get_extension_simple(tmp_path):
|
|
114
|
+
p = tmp_path / "report.pdf"
|
|
115
|
+
lf = LocalFile(path=p)
|
|
116
|
+
assert lf.get_extension() == "pdf"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_local_file_get_extension_compound(tmp_path):
|
|
120
|
+
p = tmp_path / "archive.tar.gz"
|
|
121
|
+
lf = LocalFile(path=p)
|
|
122
|
+
assert lf.get_extension() == "gz"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_local_file_get_extension_none(tmp_path):
|
|
126
|
+
p = tmp_path / "README"
|
|
127
|
+
lf = LocalFile(path=p)
|
|
128
|
+
assert lf.get_extension() == ""
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_local_file_get_extension_hidden_file(tmp_path):
|
|
132
|
+
p = tmp_path / ".gitignore"
|
|
133
|
+
lf = LocalFile(path=p)
|
|
134
|
+
assert lf.get_extension() == ""
|
|
File without changes
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
5
|
+
from wexample_file.excpetion.local_path_not_found_exception import LocalPathNotFoundException
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AbstractLocalItemPath(BaseModel, ABC):
|
|
9
|
+
"""Abstract base class for handling local file system paths.
|
|
10
|
+
|
|
11
|
+
Accepts either a string or a pathlib.Path for ``path`` and always stores a
|
|
12
|
+
resolved absolute Path (with user home expanded). This keeps comparisons and
|
|
13
|
+
downstream usage consistent regardless of how the input was provided.
|
|
14
|
+
"""
|
|
15
|
+
path: Path = Field(description="The path to the file or directory")
|
|
16
|
+
check_exists: bool = False
|
|
17
|
+
|
|
18
|
+
@field_validator("path", mode="before")
|
|
19
|
+
@classmethod
|
|
20
|
+
def _coerce_and_resolve_path(cls, v):
|
|
21
|
+
"""Coerce input into a resolved Path.
|
|
22
|
+
|
|
23
|
+
- Accepts str or Path
|
|
24
|
+
- Expands '~' and resolves to an absolute path with strict=False
|
|
25
|
+
"""
|
|
26
|
+
if isinstance(v, str):
|
|
27
|
+
v = Path(v)
|
|
28
|
+
if isinstance(v, Path):
|
|
29
|
+
return v.expanduser().resolve(strict=False)
|
|
30
|
+
raise TypeError("path must be a str or pathlib.Path")
|
|
31
|
+
|
|
32
|
+
@model_validator(mode="after")
|
|
33
|
+
def _validate_existence(self):
|
|
34
|
+
"""If check_exists is True, ensure the path exists."""
|
|
35
|
+
if self.check_exists and not self.path.exists():
|
|
36
|
+
# Defer to subclass to choose the most specific exception
|
|
37
|
+
exc = self._not_found_exc()
|
|
38
|
+
if exc is None:
|
|
39
|
+
# Fallback to a generic not-found exception
|
|
40
|
+
raise LocalPathNotFoundException(self.path)
|
|
41
|
+
raise exc
|
|
42
|
+
return self
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def _kind(self) -> str:
|
|
46
|
+
"""Return the kind of local item (e.g., 'file' or 'directory').
|
|
47
|
+
|
|
48
|
+
Subclasses must implement this to mark the class as abstract and to
|
|
49
|
+
provide a simple discriminator for debugging and representation.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
@abstractmethod
|
|
53
|
+
def _not_found_exc(self) -> Exception | None:
|
|
54
|
+
"""Return a specific 'not found' exception instance for this item type.
|
|
55
|
+
|
|
56
|
+
Subclasses should return an instance of a custom exception that best
|
|
57
|
+
represents the missing path for their type (e.g., FileNotFoundException
|
|
58
|
+
or DirectoryNotFoundException). Returning None will make the base class
|
|
59
|
+
fall back to LocalPathNotFoundException.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def remove(self) -> None:
|
|
64
|
+
"""Remove the underlying path from the filesystem.
|
|
65
|
+
|
|
66
|
+
- For a file implementation, this should delete the file.
|
|
67
|
+
- For a directory implementation, this should delete the directory
|
|
68
|
+
recursively.
|
|
69
|
+
- This operation should be idempotent: if the path does not exist,
|
|
70
|
+
the method should complete without raising.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __str__(self) -> str:
|
|
74
|
+
return str(self.path)
|
|
75
|
+
|
|
76
|
+
def __repr__(self) -> str:
|
|
77
|
+
return f"{self.__class__.__name__}(path={repr(str(self.path))}, check_exists={self.check_exists})"
|
|
78
|
+
|
|
79
|
+
def __eq__(self, other) -> bool:
|
|
80
|
+
if isinstance(other, AbstractLocalItemPath):
|
|
81
|
+
return self.path == other.path
|
|
82
|
+
if isinstance(other, (str, Path)):
|
|
83
|
+
other_path = Path(other).expanduser().resolve(strict=False)
|
|
84
|
+
return self.path == other_path
|
|
85
|
+
return NotImplemented
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from pydantic import field_validator
|
|
4
|
+
|
|
5
|
+
from wexample_file.excpetion.directory_not_found_exception import DirectoryNotFoundException
|
|
6
|
+
from wexample_file.excpetion.not_a_directory_exception import NotADirectoryException
|
|
7
|
+
from .abstract_local_item_path import AbstractLocalItemPath
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LocalDirectory(AbstractLocalItemPath):
|
|
11
|
+
"""Represents a local directory path.
|
|
12
|
+
|
|
13
|
+
The path is stored as a resolved absolute Path. If the path exists, it must
|
|
14
|
+
be a directory.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@field_validator("path")
|
|
18
|
+
@classmethod
|
|
19
|
+
def _validate_is_dir(cls, v: Path) -> Path:
|
|
20
|
+
if v.exists() and not v.is_dir():
|
|
21
|
+
raise NotADirectoryException(v)
|
|
22
|
+
return v
|
|
23
|
+
|
|
24
|
+
def _kind(self) -> str:
|
|
25
|
+
from wexample_file.const.globals import PATH_NAME_DIRECTORY
|
|
26
|
+
|
|
27
|
+
return PATH_NAME_DIRECTORY
|
|
28
|
+
|
|
29
|
+
def _not_found_exc(self):
|
|
30
|
+
return DirectoryNotFoundException(self.path)
|
|
31
|
+
|
|
32
|
+
def remove(self) -> None:
|
|
33
|
+
"""Delete the directory recursively if it exists; no-op if it doesn't.
|
|
34
|
+
|
|
35
|
+
This method is idempotent and will not raise if the directory is missing.
|
|
36
|
+
"""
|
|
37
|
+
if not self.path.exists():
|
|
38
|
+
return
|
|
39
|
+
if self.path.is_dir():
|
|
40
|
+
# Remove contents recursively
|
|
41
|
+
import shutil
|
|
42
|
+
shutil.rmtree(self.path)
|
|
43
|
+
else:
|
|
44
|
+
# If for some reason it's not a dir anymore, best-effort unlink
|
|
45
|
+
try:
|
|
46
|
+
self.path.unlink()
|
|
47
|
+
except FileNotFoundError:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
def create(self, parents: bool = True, exist_ok: bool = True) -> None:
|
|
51
|
+
if self.path.exists() and self.path.is_file():
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
self.path.mkdir(parents=parents, exist_ok=exist_ok)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from pydantic import field_validator
|
|
4
|
+
|
|
5
|
+
from wexample_file.excpetion.file_not_found_exception import FileNotFoundException
|
|
6
|
+
from wexample_file.excpetion.not_a_file_exception import NotAFileException
|
|
7
|
+
from .abstract_local_item_path import AbstractLocalItemPath
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LocalFile(AbstractLocalItemPath):
|
|
11
|
+
"""Represents a local file path.
|
|
12
|
+
|
|
13
|
+
The path is stored as a resolved absolute Path. If the path exists, it must
|
|
14
|
+
be a file.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@field_validator("path")
|
|
18
|
+
@classmethod
|
|
19
|
+
def _validate_is_file(cls, v: Path) -> Path:
|
|
20
|
+
# Only validate type when it exists; creation workflows may pass a non-existent path
|
|
21
|
+
if v.exists() and not v.is_file():
|
|
22
|
+
raise NotAFileException(v)
|
|
23
|
+
return v
|
|
24
|
+
|
|
25
|
+
def _kind(self) -> str:
|
|
26
|
+
from wexample_file.const.globals import PATH_NAME_FILE
|
|
27
|
+
|
|
28
|
+
return PATH_NAME_FILE
|
|
29
|
+
|
|
30
|
+
def _not_found_exc(self):
|
|
31
|
+
return FileNotFoundException(self.path)
|
|
32
|
+
|
|
33
|
+
def remove(self) -> None:
|
|
34
|
+
"""Delete the file if it exists; no-op if it doesn't.
|
|
35
|
+
|
|
36
|
+
This method is idempotent and will not raise if the file is missing.
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
# unlink(missing_ok=True) is available in Python 3.8+
|
|
40
|
+
self.path.unlink(missing_ok=True)
|
|
41
|
+
except TypeError:
|
|
42
|
+
# Fallback for older Python: check existence first
|
|
43
|
+
if self.path.exists():
|
|
44
|
+
self.path.unlink()
|
|
45
|
+
|
|
46
|
+
def read(self, encoding: str = "utf-8") -> str | None:
|
|
47
|
+
"""Read and return the file content as text, or None if it doesn't exist.
|
|
48
|
+
|
|
49
|
+
Parameters:
|
|
50
|
+
encoding: Text encoding used to decode file content. Defaults to 'utf-8'.
|
|
51
|
+
"""
|
|
52
|
+
if not self.path.exists() or not self.path.is_file():
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
return self.path.read_text(encoding=encoding)
|
|
56
|
+
|
|
57
|
+
def touch(self, parents: bool = True, exist_ok: bool = True) -> bool:
|
|
58
|
+
if parents:
|
|
59
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
if self.path.exists() or self.path.is_dir():
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
self.path.touch(exist_ok=exist_ok)
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
def write(self, content: str, encoding: str = "utf-8", make_parents: bool = True) -> None:
|
|
67
|
+
"""Write text content to the file, creating it if necessary.
|
|
68
|
+
"""
|
|
69
|
+
if make_parents:
|
|
70
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
if self.path.exists() and self.path.is_dir():
|
|
72
|
+
raise NotAFileException(self.path)
|
|
73
|
+
self.path.write_text(content, encoding=encoding)
|
|
74
|
+
|
|
75
|
+
def get_extension(self) -> str:
|
|
76
|
+
"""Return the last suffix without the leading dot.
|
|
77
|
+
|
|
78
|
+
Examples:
|
|
79
|
+
"archive.tar.gz" -> "gz"
|
|
80
|
+
"report.pdf" -> "pdf"
|
|
81
|
+
"README" -> ""
|
|
82
|
+
"""
|
|
83
|
+
suf = self.path.suffix
|
|
84
|
+
return suf[1:] if suf.startswith(".") else ""
|
|
85
|
+
|
|
86
|
+
def change_extension(self, new_extension: str) -> None:
|
|
87
|
+
# Normalize extension: allow callers to pass with or without dot
|
|
88
|
+
ext = new_extension.lstrip(".")
|
|
89
|
+
suffix = f".{ext}" if ext else ""
|
|
90
|
+
target = self.path.with_suffix(suffix)
|
|
91
|
+
|
|
92
|
+
self.path.replace(target)
|
|
93
|
+
|
|
94
|
+
def is_empty(self) -> bool:
|
|
95
|
+
return Path(self.path).stat().st_size == 0
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from wexample_file.excpetion.local_path_not_found_exception import LocalPathNotFoundException
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DirectoryNotFoundException(LocalPathNotFoundException):
|
|
5
|
+
error_code: str = "DIRECTORY_NOT_FOUND"
|
|
6
|
+
|
|
7
|
+
def __init__(self, path, message: str | None = None):
|
|
8
|
+
msg = message or f"Directory does not exist: {path}"
|
|
9
|
+
super().__init__(path=path, message=msg)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from wexample_file.excpetion.local_path_not_found_exception import LocalPathNotFoundException
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class FileNotFoundException(LocalPathNotFoundException):
|
|
5
|
+
error_code: str = "FILE_NOT_FOUND"
|
|
6
|
+
|
|
7
|
+
def __init__(self, path, message: str | None = None):
|
|
8
|
+
msg = message or f"File does not exist: {path}"
|
|
9
|
+
super().__init__(path=path, message=msg)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from wexample_helpers.exception.undefined_exception import UndefinedException
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class LocalPathNotFoundException(UndefinedException):
|
|
5
|
+
error_code: str = "LOCAL_PATH_NOT_FOUND"
|
|
6
|
+
|
|
7
|
+
def __init__(self, path, message: str | None = None):
|
|
8
|
+
msg = message or f"Path does not exist: {path}"
|
|
9
|
+
super().__init__(msg, data={"path": str(path)})
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from wexample_helpers.exception.undefined_exception import UndefinedException
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class NotADirectoryException(UndefinedException):
|
|
5
|
+
error_code: str = "DIRECTORY_EXPECTED"
|
|
6
|
+
|
|
7
|
+
def __init__(self, path, message: str | None = None):
|
|
8
|
+
msg = message or f"Path is not a directory: {path}"
|
|
9
|
+
super().__init__(msg, data={"path": str(path)})
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from wexample_helpers.exception.undefined_exception import UndefinedException
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class NotAFileException(UndefinedException):
|
|
5
|
+
error_code: str = "FILE_EXPECTED"
|
|
6
|
+
|
|
7
|
+
def __init__(self, path, message: str | None = None):
|
|
8
|
+
msg = message or f"Path is not a file: {path}"
|
|
9
|
+
super().__init__(msg, data={"path": str(path)})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wexample-file
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Package that allows you to manage the state of files and directories using YAML configuration files.
|
|
5
|
+
Author-email: weeger <contact@wexample.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: homepage, https://github.com/wexample/python-file
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.6
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: pip-tools
|
|
15
|
+
Requires-Dist: pydantic
|
|
16
|
+
Requires-Dist: pytest
|
|
17
|
+
Requires-Dist: wexample-config==0.0.41
|
|
18
|
+
Requires-Dist: wexample-helpers==0.0.57
|
|
19
|
+
Requires-Dist: wexample-prompt==0.0.38
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# wexample-file
|
|
23
|
+
|
|
24
|
+
Tools to manage the state of files and directories using simple YAML configuration files.
|
|
25
|
+
|
|
26
|
+
- Project: https://github.com/wexample/python-file
|
|
27
|
+
- License: MIT
|
|
28
|
+
|
|
29
|
+
Install:
|
|
30
|
+
```bash
|
|
31
|
+
pip install wexample-file
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Quick start:
|
|
35
|
+
```python
|
|
36
|
+
from wexample_file import *
|
|
37
|
+
# TODO: usage examples will go here
|
|
38
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
tests/__init__.py
|
|
5
|
+
tests/package/common/test_local_directory.py
|
|
6
|
+
tests/package/common/test_local_file.py
|
|
7
|
+
wexample_file/__init__.py
|
|
8
|
+
wexample_file.egg-info/PKG-INFO
|
|
9
|
+
wexample_file.egg-info/SOURCES.txt
|
|
10
|
+
wexample_file.egg-info/dependency_links.txt
|
|
11
|
+
wexample_file.egg-info/requires.txt
|
|
12
|
+
wexample_file.egg-info/top_level.txt
|
|
13
|
+
wexample_file/common/abstract_local_item_path.py
|
|
14
|
+
wexample_file/common/local_directory.py
|
|
15
|
+
wexample_file/common/local_file.py
|
|
16
|
+
wexample_file/const/globals.py
|
|
17
|
+
wexample_file/const/types.py
|
|
18
|
+
wexample_file/excpetion/directory_not_found_exception.py
|
|
19
|
+
wexample_file/excpetion/file_not_found_exception.py
|
|
20
|
+
wexample_file/excpetion/local_path_not_found_exception.py
|
|
21
|
+
wexample_file/excpetion/not_a_directory_exception.py
|
|
22
|
+
wexample_file/excpetion/not_a_file_exception.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|