pathpilot 0.5.0__tar.gz → 0.7.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.
- {pathpilot-0.5.0 → pathpilot-0.7.0}/PKG-INFO +4 -4
- {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/__init__.py +2 -2
- {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_file/base/_decorators.py +0 -23
- pathpilot-0.7.0/pathpilot/_file/base/_temp.py +60 -0
- pathpilot-0.7.0/pathpilot/_file/base/core.py +489 -0
- {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_file/kinds/_csv.py +16 -11
- {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_file/kinds/_excel.py +67 -32
- {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_file/kinds/_parquet.py +26 -2
- {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_file/kinds/_pickle.py +2 -2
- {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_file/kinds/_text.py +2 -6
- {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_file/kinds/_zip.py +14 -14
- {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_folder/folder/_contents/base.py +27 -17
- {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_folder/folder/_contents/subfolders.py +1 -2
- {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_folder/folder/core.py +126 -138
- pathpilot-0.7.0/pathpilot/_utils.py +11 -0
- {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/config.py +2 -2
- {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/decorators.py +4 -2
- {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/path.py +77 -12
- {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/utils.py +1 -9
- {pathpilot-0.5.0 → pathpilot-0.7.0}/pyproject.toml +29 -29
- {pathpilot-0.5.0 → pathpilot-0.7.0}/uv.lock +10 -9
- pathpilot-0.5.0/pathpilot/_file/base/core.py +0 -356
- pathpilot-0.5.0/pathpilot/_utils.py +0 -11
- {pathpilot-0.5.0 → pathpilot-0.7.0}/.gitignore +0 -0
- {pathpilot-0.5.0 → pathpilot-0.7.0}/.python-version +0 -0
- {pathpilot-0.5.0 → pathpilot-0.7.0}/LICENSE +0 -0
- {pathpilot-0.5.0 → pathpilot-0.7.0}/README.md +0 -0
- {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_file/__init__.py +0 -0
- {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_file/_utils.py +0 -0
- {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_file/base/__init__.py +0 -0
- {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_file/base/df_dispatch_file.py +0 -0
- {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_file/kinds/__init__.py +0 -0
- {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_file/kinds/_sqlite.py +0 -0
- {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_file/utils.py +0 -0
- {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_folder/__init__.py +0 -0
- {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_folder/backup.py +0 -0
- {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_folder/folder/__init__.py +0 -0
- {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_folder/folder/_contents/__init__.py +0 -0
- {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_folder/folder/_contents/files.py +0 -0
- {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_folder/utils.py +0 -0
- {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/core.py +0 -0
- {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/exceptions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: pathpilot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Simplifies file and folder manipulation
|
|
5
5
|
Project-URL: repository, https://github.com/zteinck/pathpilot
|
|
6
6
|
Project-URL: homepage, https://github.com/zteinck/pathpilot
|
|
@@ -9,10 +9,10 @@ License: MIT
|
|
|
9
9
|
License-File: LICENSE
|
|
10
10
|
Requires-Python: >=3.13
|
|
11
11
|
Requires-Dist: cachegrab>=0.2.3
|
|
12
|
-
Requires-Dist: clockwork>=0.
|
|
12
|
+
Requires-Dist: clockwork>=0.7.0
|
|
13
13
|
Requires-Dist: fastexcel>=0.19.0
|
|
14
14
|
Requires-Dist: numpy>=2.4.4
|
|
15
|
-
Requires-Dist: oddments>=0.
|
|
15
|
+
Requires-Dist: oddments>=0.10.0
|
|
16
16
|
Requires-Dist: openpyxl>=3.1.5
|
|
17
17
|
Requires-Dist: pandas>=3.0.2
|
|
18
18
|
Requires-Dist: polars>=1.39.3
|
|
@@ -9,7 +9,7 @@ from ._folder import *
|
|
|
9
9
|
from ._file import *
|
|
10
10
|
|
|
11
11
|
from .decorators import (
|
|
12
|
-
|
|
12
|
+
assert_writable,
|
|
13
13
|
assert_exists,
|
|
14
14
|
)
|
|
15
15
|
|
|
@@ -26,5 +26,5 @@ from .utils import (
|
|
|
26
26
|
get_modified_date,
|
|
27
27
|
)
|
|
28
28
|
|
|
29
|
-
__version__ = '0.
|
|
29
|
+
__version__ = '0.7.0'
|
|
30
30
|
__author__ = 'Zachary Einck <zacharyeinck@gmail.com>'
|
|
@@ -72,29 +72,6 @@ def move_file(func):
|
|
|
72
72
|
return wrapper
|
|
73
73
|
|
|
74
74
|
|
|
75
|
-
def add_affix(func):
|
|
76
|
-
|
|
77
|
-
@wraps(func)
|
|
78
|
-
def wrapper(self, text, delimiter=' ', encase=False):
|
|
79
|
-
'''
|
|
80
|
-
Parameters
|
|
81
|
-
------------
|
|
82
|
-
text : str
|
|
83
|
-
text to affix at the beginning or end of the file name
|
|
84
|
-
delimiter : str
|
|
85
|
-
character(s) separating the file name and affix
|
|
86
|
-
encase : bool
|
|
87
|
-
if True, the affix is encased in parenthesis
|
|
88
|
-
'''
|
|
89
|
-
kind = func.__name__.split('_')[-1]
|
|
90
|
-
if encase: text = f'({text})'
|
|
91
|
-
parts = [text, self.name]
|
|
92
|
-
if kind == 'suffix': parts.reverse()
|
|
93
|
-
return self.swap(name=delimiter.join(parts))
|
|
94
|
-
|
|
95
|
-
return wrapper
|
|
96
|
-
|
|
97
|
-
|
|
98
75
|
def inject_df_backend(func):
|
|
99
76
|
|
|
100
77
|
@wraps(func)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import oddments as odd
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TempFile(odd.ReprMixin):
|
|
5
|
+
'''
|
|
6
|
+
Description
|
|
7
|
+
--------------------
|
|
8
|
+
Temp File object.
|
|
9
|
+
|
|
10
|
+
Class Attributes
|
|
11
|
+
--------------------
|
|
12
|
+
None
|
|
13
|
+
|
|
14
|
+
Instance Attributes
|
|
15
|
+
--------------------
|
|
16
|
+
_dest_file : File or subclass
|
|
17
|
+
Destination file. Represents the temporary file's counterpart.
|
|
18
|
+
_temp_file : File or subclass
|
|
19
|
+
Temporary file.
|
|
20
|
+
'''
|
|
21
|
+
|
|
22
|
+
#╭-------------------------------------------------------------------------╮
|
|
23
|
+
#| Initialize Instance |
|
|
24
|
+
#╰-------------------------------------------------------------------------╯
|
|
25
|
+
|
|
26
|
+
def __init__(self, file):
|
|
27
|
+
self._dest_file = file
|
|
28
|
+
self._temp_file = self._spawn_temp_file()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
#╭-------------------------------------------------------------------------╮
|
|
32
|
+
#| Instance Methods |
|
|
33
|
+
#╰-------------------------------------------------------------------------╯
|
|
34
|
+
|
|
35
|
+
def commit(self):
|
|
36
|
+
if not self._temp_file.exists:
|
|
37
|
+
raise FileNotFoundError(
|
|
38
|
+
f'Temporary file does not exist: {self._temp_file}'
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
self.replace(self._dest_file)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _spawn_temp_file(self):
|
|
45
|
+
temp_file = self._dest_file.with_suffix('.temp')
|
|
46
|
+
|
|
47
|
+
if temp_file.exists:
|
|
48
|
+
raise FileExistsError(
|
|
49
|
+
f'Temporary file unexpectedly exists: {temp_file}'
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return temp_file
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
#╭-------------------------------------------------------------------------╮
|
|
56
|
+
#| Magic Methods |
|
|
57
|
+
#╰-------------------------------------------------------------------------╯
|
|
58
|
+
|
|
59
|
+
def __getattr__(self, name):
|
|
60
|
+
return getattr(self._temp_file, name)
|
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import filecmp
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import oddments as odd
|
|
6
|
+
|
|
7
|
+
from ...path import Path
|
|
8
|
+
from ..._folder import Folder
|
|
9
|
+
|
|
10
|
+
from ...decorators import (
|
|
11
|
+
assert_writable,
|
|
12
|
+
inject_read_only,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from ...utils import (
|
|
16
|
+
trifurcate,
|
|
17
|
+
is_file,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
from ..utils import get_size_label
|
|
22
|
+
|
|
23
|
+
from ._decorators import move_file
|
|
24
|
+
from ._temp import TempFile
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class File(Path):
|
|
28
|
+
'''
|
|
29
|
+
Description
|
|
30
|
+
--------------------
|
|
31
|
+
File object.
|
|
32
|
+
|
|
33
|
+
Class Attributes
|
|
34
|
+
--------------------
|
|
35
|
+
file_factory : func | None
|
|
36
|
+
Function that assigns new file instances to the correct subclass.
|
|
37
|
+
If None, new file instances will default to the same type as the
|
|
38
|
+
spawning instance.
|
|
39
|
+
|
|
40
|
+
Instance Attributes
|
|
41
|
+
--------------------
|
|
42
|
+
_folder : str
|
|
43
|
+
Name of folder in which the file currently resides.
|
|
44
|
+
_name : str
|
|
45
|
+
File name (does not include the file extension).
|
|
46
|
+
_extension : str
|
|
47
|
+
The file extension (does not include the period).
|
|
48
|
+
'''
|
|
49
|
+
|
|
50
|
+
#╭-------------------------------------------------------------------------╮
|
|
51
|
+
#| Initialize Instance |
|
|
52
|
+
#╰-------------------------------------------------------------------------╯
|
|
53
|
+
|
|
54
|
+
def __init__(self, path, read_only=False):
|
|
55
|
+
|
|
56
|
+
if not is_file(path):
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"'path' argument must be a file, got: {path!r}"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
self._folder, self._name, self._extension = trifurcate(path)
|
|
62
|
+
super().__init__(read_only=read_only)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
#╭-------------------------------------------------------------------------╮
|
|
66
|
+
#| Class Methods |
|
|
67
|
+
#╰-------------------------------------------------------------------------╯
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def _spawn_file(cls, *args, **kwargs):
|
|
71
|
+
'''
|
|
72
|
+
Description
|
|
73
|
+
------------
|
|
74
|
+
Ensures that newly created file instances use the appropriate
|
|
75
|
+
subclass. For example, if a CsvFile instance is converted to Excel,
|
|
76
|
+
this method guarantees the return value is an ExcelFile instance.
|
|
77
|
+
If subclass typing must be preserved regardless of changes, the
|
|
78
|
+
subclass can set the factory to None.
|
|
79
|
+
|
|
80
|
+
Note: This method must be at the class level since calling
|
|
81
|
+
self.file_factory() at the instance level passes self as the first
|
|
82
|
+
argument.
|
|
83
|
+
|
|
84
|
+
Parameters
|
|
85
|
+
------------
|
|
86
|
+
args : tuple
|
|
87
|
+
Positional arguments passed to factory callable.
|
|
88
|
+
kwargs : dict
|
|
89
|
+
Keyword arguments passed to factory callable.
|
|
90
|
+
|
|
91
|
+
Returns
|
|
92
|
+
------------
|
|
93
|
+
file : File subclass instance
|
|
94
|
+
spawned file instance
|
|
95
|
+
'''
|
|
96
|
+
ff = cls.file_factory
|
|
97
|
+
return (cls if ff is None else ff)(*args, **kwargs)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
#╭-------------------------------------------------------------------------╮
|
|
101
|
+
#| Properties |
|
|
102
|
+
#╰-------------------------------------------------------------------------╯
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def path(self):
|
|
106
|
+
''' string representation of the file including the full folder '''
|
|
107
|
+
return self._folder + self.name_with_ext
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def folder(self):
|
|
112
|
+
''' returns folder the file is currently in as a Folder object '''
|
|
113
|
+
return self._get_folder(read_only=self.read_only)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def name(self):
|
|
118
|
+
return self._name
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def name_with_extension(self):
|
|
123
|
+
''' file name including file extension but exlcuding the folder '''
|
|
124
|
+
return self.name + '.' + self.extension
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def name_with_ext(self):
|
|
129
|
+
''' self.name_with_extension alias '''
|
|
130
|
+
return self.name_with_extension
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def full_name(self):
|
|
135
|
+
''' self.name_with_extension alias '''
|
|
136
|
+
return self.name_with_extension
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def extension(self):
|
|
141
|
+
''' shorthand self.extension alias '''
|
|
142
|
+
return self._extension
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def ext(self):
|
|
147
|
+
''' shorthand self.extension alias '''
|
|
148
|
+
return self._extension
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def size(self):
|
|
153
|
+
''' the current size of the file expressed in bytes '''
|
|
154
|
+
if self.exists:
|
|
155
|
+
return os.stat(self.path).st_size
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def size_label(self):
|
|
160
|
+
''' the current size of the file expressed in bytes '''
|
|
161
|
+
if self.exists:
|
|
162
|
+
return get_size_label(self.size)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def meta_data(self):
|
|
167
|
+
result = super().meta_data.copy()
|
|
168
|
+
|
|
169
|
+
result.update({
|
|
170
|
+
'label': 'file',
|
|
171
|
+
'folder': self.folder.path,
|
|
172
|
+
'full_name': self.full_name,
|
|
173
|
+
'extension': self.extension,
|
|
174
|
+
'size': self.size,
|
|
175
|
+
'size_label': self.size_label,
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
return result
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
#╭-------------------------------------------------------------------------╮
|
|
182
|
+
#| Magic Methods |
|
|
183
|
+
#╰-------------------------------------------------------------------------╯
|
|
184
|
+
|
|
185
|
+
def __eq__(self, other):
|
|
186
|
+
a, b = self.path, str(other)
|
|
187
|
+
return a == b and filecmp.cmp(a, b)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def __ne__(self, other):
|
|
191
|
+
return not self.__eq__(other)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
#╭-------------------------------------------------------------------------╮
|
|
195
|
+
#| Instance Methods |
|
|
196
|
+
#╰-------------------------------------------------------------------------╯
|
|
197
|
+
|
|
198
|
+
@inject_read_only
|
|
199
|
+
def spawn(self, *args, **kwargs):
|
|
200
|
+
return self._spawn_file(*args, **kwargs)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def read(self, *args, **kwargs):
|
|
204
|
+
raise NotImplementedError(
|
|
205
|
+
f"'{self.__class__.__name__}.read()' is not implemented."
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@assert_writable
|
|
210
|
+
def save(self, *args, **kwargs):
|
|
211
|
+
self._save(*args, **kwargs)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@assert_writable
|
|
215
|
+
def touch(self):
|
|
216
|
+
''' create file with no content '''
|
|
217
|
+
with open(self.path, mode='wb') as file:
|
|
218
|
+
pass
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@assert_writable
|
|
222
|
+
def delete(self):
|
|
223
|
+
''' delete file if it exists '''
|
|
224
|
+
if self.exists:
|
|
225
|
+
os.remove(self.path)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def open(self):
|
|
229
|
+
''' open file in default program '''
|
|
230
|
+
os.startfile(self.path)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def trifurcate_and_fill(self, path):
|
|
234
|
+
''' trifurcates file and fills gaps with instance attributes '''
|
|
235
|
+
folder, name, extension = trifurcate(path, default_folder=False)
|
|
236
|
+
|
|
237
|
+
filled_path = ''.join((
|
|
238
|
+
(folder or self.folder.path),
|
|
239
|
+
(name or self.name),
|
|
240
|
+
'.',
|
|
241
|
+
(extension or self.extension),
|
|
242
|
+
))
|
|
243
|
+
|
|
244
|
+
return self.spawn(filled_path)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@move_file
|
|
248
|
+
def replace(self, destination):
|
|
249
|
+
'''
|
|
250
|
+
Description
|
|
251
|
+
------------
|
|
252
|
+
...
|
|
253
|
+
|
|
254
|
+
Parameters
|
|
255
|
+
------------
|
|
256
|
+
...
|
|
257
|
+
|
|
258
|
+
Returns
|
|
259
|
+
------------
|
|
260
|
+
None
|
|
261
|
+
'''
|
|
262
|
+
os.replace(self.path, destination.path)
|
|
263
|
+
return destination
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@move_file
|
|
267
|
+
def rename(self, destination):
|
|
268
|
+
'''
|
|
269
|
+
Description
|
|
270
|
+
------------
|
|
271
|
+
Rename file.
|
|
272
|
+
|
|
273
|
+
Parameters
|
|
274
|
+
------------
|
|
275
|
+
...
|
|
276
|
+
|
|
277
|
+
Returns
|
|
278
|
+
------------
|
|
279
|
+
None
|
|
280
|
+
'''
|
|
281
|
+
os.rename(self.path, destination.path)
|
|
282
|
+
return destination
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@move_file
|
|
286
|
+
def cut(self, destination):
|
|
287
|
+
''' cut and paste the file to a new location '''
|
|
288
|
+
shutil.move(self.path, destination.path)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@move_file
|
|
292
|
+
def copy(self, destination):
|
|
293
|
+
''' copy the file to a new location '''
|
|
294
|
+
shutil.copyfile(self.path, destination.path)
|
|
295
|
+
return destination
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def require(self, destination):
|
|
299
|
+
''' special case of self.copy where file is copied to destination ONLY
|
|
300
|
+
if it does not already exist '''
|
|
301
|
+
return self.copy(
|
|
302
|
+
destination,
|
|
303
|
+
overwrite=False,
|
|
304
|
+
raise_on_exist=False,
|
|
305
|
+
raise_on_overwrite=False,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@assert_writable
|
|
310
|
+
def zip(self, name=None, **kwargs):
|
|
311
|
+
''' zips a single file '''
|
|
312
|
+
if not self.exists:
|
|
313
|
+
raise FileNotFoundError(
|
|
314
|
+
f'Cannot zip file because it does not exist:\n{self.path}'
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
from ..kinds import ZipFile
|
|
318
|
+
|
|
319
|
+
zip_file = ZipFile(
|
|
320
|
+
self
|
|
321
|
+
.trifurcate_and_fill(name or self.path)
|
|
322
|
+
.with_extension('zip')
|
|
323
|
+
.path
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
zip_file.zip(self.path, **kwargs)
|
|
327
|
+
|
|
328
|
+
return zip_file
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def swap(self, folder=None, name=None, extension=None, **kwargs):
|
|
332
|
+
'''
|
|
333
|
+
Description
|
|
334
|
+
------------
|
|
335
|
+
Returns a new file instance with one or more of the current instance's
|
|
336
|
+
key attributes swapped out and replaced with another value.
|
|
337
|
+
|
|
338
|
+
Parameters
|
|
339
|
+
------------
|
|
340
|
+
folder : None | str | Folder
|
|
341
|
+
The folder to use for the new instance. If None, the current
|
|
342
|
+
instance's folder is left intact. If a string is given, it is
|
|
343
|
+
interpreted as a path.
|
|
344
|
+
name : None | str
|
|
345
|
+
The file name (without extension) to use for the new instance. If
|
|
346
|
+
None, the current instance's name is left intact.
|
|
347
|
+
extension : None | str
|
|
348
|
+
The file extension to use for the new instance, with or without
|
|
349
|
+
a leading period (e.g. 'txt' or '.txt'). If None, the current
|
|
350
|
+
instance's extension is left intact.
|
|
351
|
+
|
|
352
|
+
Returns
|
|
353
|
+
------------
|
|
354
|
+
result : File or subclass
|
|
355
|
+
A new instance of the same type as the current instance, with the
|
|
356
|
+
specified attributes replaced.
|
|
357
|
+
'''
|
|
358
|
+
|
|
359
|
+
# validate folder argument
|
|
360
|
+
(
|
|
361
|
+
odd.Validator(
|
|
362
|
+
types=(str, Folder),
|
|
363
|
+
allow_none=True,
|
|
364
|
+
allow_blank=False,
|
|
365
|
+
require_stripped=True,
|
|
366
|
+
)
|
|
367
|
+
.validate(
|
|
368
|
+
folder=folder
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
# normalize folder argument
|
|
373
|
+
if folder is None:
|
|
374
|
+
folder = self._folder
|
|
375
|
+
elif isinstance(folder, str):
|
|
376
|
+
if folder[-1] != '/':
|
|
377
|
+
folder += '/'
|
|
378
|
+
elif isinstance(folder, Folder):
|
|
379
|
+
folder = folder.path
|
|
380
|
+
else:
|
|
381
|
+
raise TypeError
|
|
382
|
+
|
|
383
|
+
# normalize name argument
|
|
384
|
+
(
|
|
385
|
+
odd.Validator(
|
|
386
|
+
types=str,
|
|
387
|
+
allow_none=True,
|
|
388
|
+
require_stripped=True,
|
|
389
|
+
)
|
|
390
|
+
.validate(
|
|
391
|
+
name=name
|
|
392
|
+
)
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
if name is None:
|
|
396
|
+
name = self._name
|
|
397
|
+
|
|
398
|
+
# normalize extension argument
|
|
399
|
+
(
|
|
400
|
+
odd.Validator(
|
|
401
|
+
types=str,
|
|
402
|
+
allow_none=True,
|
|
403
|
+
allow_blank=False,
|
|
404
|
+
)
|
|
405
|
+
.validate(
|
|
406
|
+
extension=extension
|
|
407
|
+
)
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
extension = (
|
|
411
|
+
self._extension
|
|
412
|
+
if extension is None
|
|
413
|
+
else extension.replace('.', '')
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
path = ''.join((folder, name, '.', extension))
|
|
417
|
+
|
|
418
|
+
return self.spawn(path, **kwargs)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
@assert_writable
|
|
422
|
+
def to_temp_file(self):
|
|
423
|
+
return TempFile(self)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def with_folder(self, folder):
|
|
427
|
+
return self.swap(folder=folder)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def with_name(self, name):
|
|
431
|
+
return self.swap(name=name)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def with_extension(self, extension):
|
|
435
|
+
return self.swap(extension=extension)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def with_parent(self):
|
|
439
|
+
return self.folder.parent.join_file(self.name_with_ext)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def with_sibling(self, *args, **kwargs):
|
|
443
|
+
return self.folder.join_file(*args, expected_descent=0, **kwargs)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def with_child(self, name):
|
|
447
|
+
return self.with_descendant(name, expected_descent=1)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def with_subfolder(self, name):
|
|
451
|
+
return self.with_child(name)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def with_descendant(self, *args, **kwargs):
|
|
455
|
+
file = (
|
|
456
|
+
self.folder
|
|
457
|
+
.join_folder(*args, **kwargs)
|
|
458
|
+
.join_file(self.name_with_ext)
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
return file
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
@assert_writable
|
|
465
|
+
def nest(self, *args, **kwargs):
|
|
466
|
+
nested_file = self.with_descendant(*args, **kwargs)
|
|
467
|
+
return self.replace(nested_file)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
@assert_writable
|
|
471
|
+
def unnest(self):
|
|
472
|
+
unnested_file = self.with_parent()
|
|
473
|
+
return self.replace(unnested_file)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _save(self, *args, **kwargs):
|
|
477
|
+
raise NotImplementedError(
|
|
478
|
+
f"'{self.__class__.__name__}._save()' is not implemented."
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _get_folder(self, read_only):
|
|
483
|
+
return Folder(self._folder, read_only=read_only)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _on_read_only_toggle(self):
|
|
487
|
+
''' creates the folder if read-only is toggled to False '''
|
|
488
|
+
if not self.read_only:
|
|
489
|
+
self.folder.create()
|
|
@@ -3,7 +3,11 @@ import oddments as odd
|
|
|
3
3
|
import pandas as pd
|
|
4
4
|
import polars as pl
|
|
5
5
|
|
|
6
|
-
from ...decorators import
|
|
6
|
+
from ...decorators import (
|
|
7
|
+
assert_exists,
|
|
8
|
+
assert_writable,
|
|
9
|
+
)
|
|
10
|
+
|
|
7
11
|
from ..base import DfDispatchFile
|
|
8
12
|
|
|
9
13
|
|
|
@@ -21,6 +25,16 @@ class CsvFile(DfDispatchFile):
|
|
|
21
25
|
#| Instance Methods |
|
|
22
26
|
#╰-------------------------------------------------------------------------╯
|
|
23
27
|
|
|
28
|
+
@assert_exists
|
|
29
|
+
def scan(self, **kwargs):
|
|
30
|
+
return pl.scan_csv(self.path, **kwargs)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@assert_writable
|
|
34
|
+
def sink(self, lf, **kwargs):
|
|
35
|
+
lf.sink_csv(self.path, **kwargs)
|
|
36
|
+
|
|
37
|
+
|
|
24
38
|
def _read_with_pandas(self, **kwargs):
|
|
25
39
|
defaults = {
|
|
26
40
|
'encoding': 'ISO-8859-1',
|
|
@@ -41,13 +55,4 @@ class CsvFile(DfDispatchFile):
|
|
|
41
55
|
|
|
42
56
|
|
|
43
57
|
def _save_with_polars(self, obj, **kwargs):
|
|
44
|
-
obj.write_csv(self.path, **kwargs)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def scan(self, **kwargs):
|
|
48
|
-
return pl.scan_csv(self.path, **kwargs)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
@check_read_only
|
|
52
|
-
def sink(self, lf, **kwargs):
|
|
53
|
-
lf.sink_csv(self.path, **kwargs)
|
|
58
|
+
obj.write_csv(self.path, **kwargs)
|