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.
Files changed (42) hide show
  1. {pathpilot-0.5.0 → pathpilot-0.7.0}/PKG-INFO +4 -4
  2. {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/__init__.py +2 -2
  3. {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_file/base/_decorators.py +0 -23
  4. pathpilot-0.7.0/pathpilot/_file/base/_temp.py +60 -0
  5. pathpilot-0.7.0/pathpilot/_file/base/core.py +489 -0
  6. {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_file/kinds/_csv.py +16 -11
  7. {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_file/kinds/_excel.py +67 -32
  8. {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_file/kinds/_parquet.py +26 -2
  9. {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_file/kinds/_pickle.py +2 -2
  10. {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_file/kinds/_text.py +2 -6
  11. {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_file/kinds/_zip.py +14 -14
  12. {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_folder/folder/_contents/base.py +27 -17
  13. {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_folder/folder/_contents/subfolders.py +1 -2
  14. {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_folder/folder/core.py +126 -138
  15. pathpilot-0.7.0/pathpilot/_utils.py +11 -0
  16. {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/config.py +2 -2
  17. {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/decorators.py +4 -2
  18. {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/path.py +77 -12
  19. {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/utils.py +1 -9
  20. {pathpilot-0.5.0 → pathpilot-0.7.0}/pyproject.toml +29 -29
  21. {pathpilot-0.5.0 → pathpilot-0.7.0}/uv.lock +10 -9
  22. pathpilot-0.5.0/pathpilot/_file/base/core.py +0 -356
  23. pathpilot-0.5.0/pathpilot/_utils.py +0 -11
  24. {pathpilot-0.5.0 → pathpilot-0.7.0}/.gitignore +0 -0
  25. {pathpilot-0.5.0 → pathpilot-0.7.0}/.python-version +0 -0
  26. {pathpilot-0.5.0 → pathpilot-0.7.0}/LICENSE +0 -0
  27. {pathpilot-0.5.0 → pathpilot-0.7.0}/README.md +0 -0
  28. {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_file/__init__.py +0 -0
  29. {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_file/_utils.py +0 -0
  30. {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_file/base/__init__.py +0 -0
  31. {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_file/base/df_dispatch_file.py +0 -0
  32. {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_file/kinds/__init__.py +0 -0
  33. {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_file/kinds/_sqlite.py +0 -0
  34. {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_file/utils.py +0 -0
  35. {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_folder/__init__.py +0 -0
  36. {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_folder/backup.py +0 -0
  37. {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_folder/folder/__init__.py +0 -0
  38. {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_folder/folder/_contents/__init__.py +0 -0
  39. {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_folder/folder/_contents/files.py +0 -0
  40. {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/_folder/utils.py +0 -0
  41. {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/core.py +0 -0
  42. {pathpilot-0.5.0 → pathpilot-0.7.0}/pathpilot/exceptions.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: pathpilot
3
- Version: 0.5.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.6.1
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.9.1
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
- check_read_only,
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.5.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 check_read_only
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)