pathpilot 0.3.3__tar.gz → 0.3.4__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 (23) hide show
  1. {pathpilot-0.3.3 → pathpilot-0.3.4}/PKG-INFO +3 -3
  2. {pathpilot-0.3.3 → pathpilot-0.3.4}/pathpilot/__init__.py +1 -1
  3. {pathpilot-0.3.3 → pathpilot-0.3.4}/pathpilot/_file/_excel.py +161 -130
  4. {pathpilot-0.3.3 → pathpilot-0.3.4}/pathpilot/_file/_sqlite.py +25 -12
  5. {pathpilot-0.3.3 → pathpilot-0.3.4}/pathpilot/_file/_zip.py +36 -21
  6. {pathpilot-0.3.3 → pathpilot-0.3.4}/pathpilot/_file/base.py +5 -3
  7. {pathpilot-0.3.3 → pathpilot-0.3.4}/pathpilot/_folder/backup.py +2 -2
  8. {pathpilot-0.3.3 → pathpilot-0.3.4}/pathpilot/_folder/base.py +59 -40
  9. pathpilot-0.3.4/pathpilot/exceptions.py +7 -0
  10. {pathpilot-0.3.3 → pathpilot-0.3.4}/pathpilot/utils.py +2 -1
  11. {pathpilot-0.3.3 → pathpilot-0.3.4}/pyproject.toml +3 -3
  12. pathpilot-0.3.3/pathpilot/exceptions.py +0 -6
  13. {pathpilot-0.3.3 → pathpilot-0.3.4}/LICENSE +0 -0
  14. {pathpilot-0.3.3 → pathpilot-0.3.4}/README.md +0 -0
  15. {pathpilot-0.3.3 → pathpilot-0.3.4}/pathpilot/_file/__init__.py +0 -0
  16. {pathpilot-0.3.3 → pathpilot-0.3.4}/pathpilot/_file/_csv.py +0 -0
  17. {pathpilot-0.3.3 → pathpilot-0.3.4}/pathpilot/_file/_pickle.py +0 -0
  18. {pathpilot-0.3.3 → pathpilot-0.3.4}/pathpilot/_file/_text.py +0 -0
  19. {pathpilot-0.3.3 → pathpilot-0.3.4}/pathpilot/_file/utils.py +0 -0
  20. {pathpilot-0.3.3 → pathpilot-0.3.4}/pathpilot/_folder/__init__.py +0 -0
  21. {pathpilot-0.3.3 → pathpilot-0.3.4}/pathpilot/_folder/utils.py +0 -0
  22. {pathpilot-0.3.3 → pathpilot-0.3.4}/pathpilot/core.py +0 -0
  23. {pathpilot-0.3.3 → pathpilot-0.3.4}/pathpilot/decorators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pathpilot
3
- Version: 0.3.3
3
+ Version: 0.3.4
4
4
  Summary: Library that facilitates file and folder manipulation in Python.
5
5
  Home-page: https://github.com/zteinck/pathpilot
6
6
  License: MIT
@@ -16,9 +16,9 @@ Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
17
  Requires-Dist: XlsxWriter
18
18
  Requires-Dist: cachegrab (>=0.2.2)
19
- Requires-Dist: clockwork (>=0.3.4)
19
+ Requires-Dist: clockwork (>=0.4.0)
20
20
  Requires-Dist: numpy
21
- Requires-Dist: oddments (>=0.3.1)
21
+ Requires-Dist: oddments (>=0.4.0)
22
22
  Requires-Dist: pandas
23
23
  Project-URL: Repository, https://github.com/zteinck/pathpilot
24
24
  Description-Content-Type: text/markdown
@@ -3,5 +3,5 @@ from .decorators import *
3
3
  from .exceptions import *
4
4
  from .utils import *
5
5
 
6
- __version__ = '0.3.3'
6
+ __version__ = '0.3.4'
7
7
  __author__ = 'Zachary Einck <zacharyeinck@gmail.com>'
@@ -30,22 +30,22 @@ class ExcelFile(FileBase):
30
30
  active_sheet : xlsxwriter.worksheet.Worksheet
31
31
  The active worksheet
32
32
  sheet_cache : dict
33
- Dictionary where the keys are worksheet names and values are the worksheet
34
- objects. It is important to note that the keys are the original sheet names
35
- passed by the user as opposed to the actual name that appears in the excel
36
- file and worksheet.name attribute because the latter is truncated by the 31
37
- character limit and/or will have a page prefix added per the 'number_tabs'
38
- argument. This setup allows the user to access the sheet using the original
39
- name.
33
+ Dictionary where the keys are worksheet names and values are the
34
+ worksheet objects. It is important to note that the keys are the
35
+ original sheet names passed by the user as opposed to the actual name
36
+ that appears in the excel file and worksheet.name attribute because the
37
+ latter is truncated by the 31 character limit and/or will have a page
38
+ prefix added per the 'number_tabs' argument. This setup allows the user
39
+ to access the sheet using the original name.
40
40
  formats : dict
41
- keys are strings representing names of formats and values are dictionaries
42
- containing format parameters. For Example, {{'bold': {'bold': True}}. These
43
- are intended to be frequently used formats that can be both used individually
44
- and as building blocks for more complex formats (see self.add_format for more
45
- information.)
41
+ Keys are strings representing names of formats and values are
42
+ dictionaries containing format parameters. For Example, {{'bold':
43
+ {'bold': True}}. These are intended to be frequently used formats that
44
+ can be both used individually and as building blocks for more complex
45
+ formats (see self.add_format for more information.)
46
46
  format_cache : dict
47
- keys are hashes of sorted dictionaries containing format parameters and
48
- values are xlsxwriter.format.Format. This is done so that only one
47
+ keys are hashes of sorted dictionaries containing format parameters
48
+ and values are xlsxwriter.format.Format. This is done so that only one
49
49
  format instance will be created per unique format used.
50
50
  number_tabs : bool
51
51
  if True, tabs in the workbook will have a number prefix. For example,
@@ -53,7 +53,8 @@ class ExcelFile(FileBase):
53
53
  page : int
54
54
  keeps track of the current tab number
55
55
  verbose : bool
56
- if True, information is printed when a worksheet is created or written to.
56
+ if True, information is printed when a worksheet is created or written
57
+ to.
57
58
 
58
59
  Note:
59
60
  --------------------
@@ -299,9 +300,18 @@ class ExcelFile(FileBase):
299
300
  'header': {'bold': True,'bottom': True, 'text_wrap': True},
300
301
 
301
302
  # Conditional
302
- 'conditional_red': {'bg_color': '#FFC7CE', 'font_color': '#9C0006'},
303
- 'conditional_green': {'bg_color': '#C6EFCE', 'font_color': '#006100'},
304
- 'conditional_yellow': {'bg_color': '#FFEB9C', 'font_color': '#9C6500'},
303
+ 'conditional_red': {
304
+ 'bg_color': '#FFC7CE',
305
+ 'font_color': '#9C0006'
306
+ },
307
+ 'conditional_green': {
308
+ 'bg_color': '#C6EFCE',
309
+ 'font_color': '#006100'
310
+ },
311
+ 'conditional_yellow': {
312
+ 'bg_color': '#FFEB9C',
313
+ 'font_color': '#9C6500'
314
+ },
305
315
  }
306
316
 
307
317
  return format_presets
@@ -316,8 +326,8 @@ class ExcelFile(FileBase):
316
326
  Description
317
327
  ------------
318
328
  Called when an attribute is referenced that does not exist. In this
319
- case, attributes that do not exist will be assumed to be active worksheet
320
- object attributes (e.g. '.hide_gridlines').
329
+ case, attributes that do not exist will be assumed to be active
330
+ worksheet object attributes (e.g. '.hide_gridlines').
321
331
 
322
332
  Parameters
323
333
  ------------
@@ -387,11 +397,12 @@ class ExcelFile(FileBase):
387
397
  Parameters
388
398
  ------------
389
399
  args : pd.DataFrame | list | dict
390
- • if list, each dataframe in the list is written to a separate worksheet.
391
- • if dict, the keys are considered sheet names and the values should be
392
- dataframes. (i.e. {'My Sheet 1': df1, 'My Sheet 2': df2}).
393
- You may not pass a sheets argument when passing a dictionary
394
- as the args argument.
400
+ • if list, each dataframe in the list is written to a separate
401
+ worksheet.
402
+ if dict, the keys are considered sheet names and the values
403
+ should be DataFrames. (i.e. {'My Sheet 1': df1, 'My Sheet 2':
404
+ df2}). You may not pass a sheets argument when passing a
405
+ dictionary as the args argument.
395
406
  sheets : list | str | None
396
407
  Mist of sheet names. Must be None when 'args' is a dictionary.
397
408
  kwargs : dict
@@ -407,15 +418,16 @@ class ExcelFile(FileBase):
407
418
  if isinstance(args, dict):
408
419
  if sheets is not None:
409
420
  raise ValueError(
410
- "'sheets' argument must be None when a dictionary is passed"
421
+ "'sheets' argument must be None "
422
+ "when a dictionary is passed"
411
423
  )
412
424
  sheets = list(args.keys())
413
425
  args = list(args.values())
414
426
  else:
415
- args = odd.to_iter(args)
427
+ args = odd.ensure_list(args)
416
428
 
417
429
  if sheets is not None:
418
- sheets = odd.to_iter(sheets)
430
+ sheets = odd.ensure_list(sheets)
419
431
  if len(args) != len(sheets):
420
432
  raise ValueError(
421
433
  f"length of 'args' ({len(args)}) != length of 'sheets' "
@@ -482,44 +494,44 @@ class ExcelFile(FileBase):
482
494
  ------------
483
495
  start_cell : str | tuple
484
496
  Individual cell range to be used as the starting position for the
485
- fill. May be a standard excel range (e.g. 'A2') or a tuple of length
486
- two (column, row) (e.g. (1, 2) | ('A', 2)).
497
+ fill. May be a standard excel range (e.g. 'A2') or a tuple of
498
+ length two (column, row) (e.g. (1, 2) | ('A', 2)).
487
499
  data : 1D list | 2D list | single value | pd.DataFrame | pd.Series
488
- Data to be written to excel worksheet starting in start_cell.
489
- Each sub-list is treated as row data and written in sequential order.
490
- For example, if start_cell = 'A1' and data = [[1, 2, 3], ['a', 'b', 'c']],
491
- then row 1 and 2 will be filled with 1, 2, 3 and 'a', 'b', 'c', respectively.
492
- formatting : str | dict | one-dimensional list | one-dimensional tuple | None
500
+ Data to be written to excel worksheet starting in start_cell. Each
501
+ sub-list is treated as row data and written in sequential order.
502
+ For example, if start_cell = 'A1' and data = [[1, 2, 3], ['a', 'b',
503
+ 'c']], then row 1 and 2 will be filled with 1, 2, 3 and 'a', 'b',
504
+ 'c', respectively.
505
+ formatting : str | dict | list(1D) | tuple(1D) | None
493
506
  Excel formatting to be applied to data. Supported types include:
494
- • str ➜ Strings are interpreted as self.formats dictionary keys.
495
- Passing a single string (e.g. formatting='bold') causes
496
- the corresponding format to be be universally applied to
497
- all data cells.
507
+ • str ➜ Strings are interpreted as self.formats dictionary
508
+ keys. Passing a single string (e.g. formatting='bold')
509
+ causes the corresponding format to be be universally
510
+ applied to all data cells.
498
511
  • dict ➜ Dictionaries are interpreted as format parameters.
499
- Passing a single dictionary (e.g. formatting =
500
- {'bold': True, 'num_format': '#,##0'}) causes the
501
- corresponding format to be be universally applied
502
- to all data cells.
512
+ Passing a single dictionary (e.g. formatting = {'bold':
513
+ True, 'num_format': '#,##0'}) causes the corresponding
514
+ format to be be universally applied to all data cells.
503
515
  • list | tuple ➜ formats included in a list/tuuple are applied
504
- to columns in sequential order. If the length
505
- of the list/tuple is shorter than the number
506
- of columns then no format is applied to the
507
- remaining columns.
516
+ to columns in sequential order. If the length of the list
517
+ or tuple is shorter than the number of columns then no
518
+ format is applied to the remaining columns.
508
519
  • None ➜ no formatting is applied.
509
520
  inverse : bool
510
- If True, the 2D data is inverted such that each sub-list is treated
511
- as column data as opposed to row data under the default behavior.
512
- (e.g. [[1, 2, 3], ['a', 'b', 'c']] ➜ [[1, 'a'], [2, 'b'], [3, 'c']]
521
+ If True, the 2D data is inverted such that each sub-list is
522
+ treated as column data as opposed to row data under the default
523
+ behavior. For example:
524
+ [[1, 2, 3], ['a', 'b', 'c']] ➜ [[1, 'a'], [2, 'b'], [3, 'c']]
513
525
  repeat : int
514
526
  If 'data' argument is a single data element then it will be repeated
515
527
  or duplicated this number of times.
516
528
  sheet : str
517
- If None, self.active_sheet is utilized. If self.active_sheet has not
518
- yet been assigned, it will be assigned to a newly created
519
- blank worksheet named 'Sheet1'.
520
- If not None, self.active_sheet will be assigned to the passed sheet name.
521
- If the passed name does not correspond to an existing sheet,
522
- it will be created.
529
+ If None, self.active_sheet is utilized. If self.active_sheet has
530
+ not yet been assigned, it will be assigned to a newly created
531
+ blank worksheet named 'Sheet1'.
532
+ If not None, self.active_sheet will be assigned to the passed
533
+ sheet name. If the passed name does not correspond to an
534
+ existing sheet, it will be created.
523
535
  outer_border : bool
524
536
  If True, data is encased in an outer border.
525
537
 
@@ -666,60 +678,60 @@ class ExcelFile(FileBase):
666
678
  data_format : ↑
667
679
  Special cases:
668
680
  • 'auto' ➜ formatting is automatically applied to numeric, percent,
669
- and date fields.
681
+ and date fields.
670
682
  • dict ➜ if the dictionary keys are only comprised of 'df' index or
671
- column names then the values are treated like format
672
- parameters and formatting is only applied to those columns
673
- included in the keys. If not all the key values are column
674
- names then the dictionary receives the default treatment
675
- outlined in the self.write documentation. (e.g.
676
- {'Price': 'commas', 'Total': {'bold': True}})
683
+ column names then the values are treated like format parameters
684
+ and formatting is only applied to those columns included in the
685
+ keys. If not all the key values are column names then the
686
+ dictionary receives the default treatment outlined in the
687
+ self.write documentation. (e.g.{'Price': 'commas', 'Total':
688
+ {'bold': True}})
677
689
  Other cases:
678
690
  see self.write 'formatting' argument documentation.
679
691
  column_widths : 'auto' | list | tuple | dict
680
- • 'auto' ➜ xlsxwriter does not support auto-fitting column widths so
681
- this attempts replicate it by setting the column width
682
- according to the length of the values in each column
683
- (up to a certain limit).
692
+ • 'auto' ➜ xlsxwriter does not support auto-fitting column widths
693
+ so this attempts replicate it by setting the column width
694
+ according to the length of the values in each column (up to a
695
+ certain limit).
684
696
  • list | tuple ➜ widths are applied to columns in sequential order.
685
- If the length of the list/tuple is shorter than the
686
- number of columns then the width is not set on the
687
- remaining columns.
697
+ If the length of the list/tuple is shorter than the
698
+ number of columns then the width is not set on the
699
+ remaining columns.
688
700
  • dict ➜ dictionary where keys are DataFrame column names and values
689
- are column widths. Any column names excluded from the
690
- dictionary will not have their widths set.
701
+ are column widths. Any column names excluded from the dictionary
702
+ will not have their widths set.
691
703
  date_format : None | str | dict
692
704
  Defines how date-like columns are parsed when data_format='auto'
693
705
  (e.g. '%Y-%m-%d'). Options include:
694
706
  • None ➜ format is inferred.
695
707
  • str ➜ used for all date-like columns.
696
708
  • dict ➜ dictionary where keys are DataFrame column names and values
697
- are formats. Any column names excluded from the dictionary
698
- default to None.
709
+ are formats. Any column names excluded from the dictionary
710
+ default to None.
699
711
  normalize : bool
700
712
  if True, any date columns where the hours, minutes, seconds,
701
- microseconds are all set to zero (midnight) will be converted
702
- from a datetime to date.
713
+ microseconds are all set to zero (midnight) will be converted
714
+ from a datetime to date.
703
715
  autofilter : bool
704
716
  if True, a filter will be applied to the column headers.
705
717
  raise_on_empty : bool
706
718
  if True and the 'df' argument is empty, an exception will be raised.
707
719
  if False, the 'df' columns will be written to an otherwise empty
708
- worksheet.
720
+ worksheet.
709
721
  total_row : bool
710
- if True, a row is added at the bottom reflecting the sum of each numeric
711
- column.
722
+ if True, a row is added at the bottom reflecting the sum of each
723
+ numeric column.
712
724
  total_row_format : ^
713
- format applied if 'total_row' is True. If None and data_format='auto',
714
- then the same formatting will be applied to the total row plus bold
715
- and a top border.
725
+ format applied if 'total_row' is True. If None and data_format =
726
+ 'auto', then the same formatting will be applied to the total row
727
+ plus bold and a top border.
716
728
  total_column : bool
717
- if True, a column is added at the end reflecting the sum of all numeric
718
- values in each row.
729
+ if True, a column is added at the end reflecting the sum of all
730
+ numeric values in each row.
719
731
  total_column_format : ^
720
- format applied if 'total_column' is True. If None and data_format='auto',
721
- then the same formatting will be applied to the total column plus bold and
722
- a left border.
732
+ format applied if 'total_column' is True. If None and data_format =
733
+ 'auto', then the same formatting will be applied to the total column
734
+ plus bold and a left border.
723
735
 
724
736
  Returns
725
737
  ------------
@@ -730,7 +742,7 @@ class ExcelFile(FileBase):
730
742
 
731
743
  odd.validate_value(
732
744
  value=date_format,
733
- attr='date_format',
745
+ name='date_format',
734
746
  types=(str, dict),
735
747
  none_ok=True
736
748
  )
@@ -740,7 +752,7 @@ class ExcelFile(FileBase):
740
752
  raise NotImplementedError
741
753
 
742
754
  # Reset index
743
- if odd.get_index_names(df):
755
+ if odd.has_named_index(df):
744
756
  df.reset_index(inplace=True)
745
757
 
746
758
  # Check if empty
@@ -758,7 +770,7 @@ class ExcelFile(FileBase):
758
770
  total_column = False
759
771
 
760
772
  # Check for duplicate column names
761
- odd.verify_no_duplicates(df=df, attr='columns')
773
+ odd.verify_unique(df, column_names=True)
762
774
 
763
775
  # Add a total column to dataframe
764
776
  if total_column:
@@ -880,11 +892,14 @@ class ExcelFile(FileBase):
880
892
  if fmt is None:
881
893
  datelike_columns.remove(k)
882
894
  else:
883
- df[k] = df[k].apply(odd.ignore_nan(lambda x: \
884
- datetime.datetime.strptime(x, fmt)
895
+ df[k] = df[k].apply(odd.ignore_nan(
896
+ lambda x: datetime.datetime.strptime(x, fmt)
885
897
  ))
886
898
  datetime_columns.add(k)
887
- elif all(issubclass(x, datetime.datetime) for x in types):
899
+ elif all(
900
+ issubclass(x, datetime.datetime)
901
+ for x in types
902
+ ):
888
903
  datetime_columns.add(k)
889
904
  else:
890
905
  datelike_columns.remove(k)
@@ -936,7 +951,7 @@ class ExcelFile(FileBase):
936
951
 
937
952
  if total_column_format == 'auto':
938
953
  data_format[total_column_name] = \
939
- self.add_format(odd.to_iter(
954
+ self.add_format(odd.ensure_list(
940
955
  data_format.pop(total_column_name, [])
941
956
  ) + ['bold','left'])
942
957
  else:
@@ -964,8 +979,12 @@ class ExcelFile(FileBase):
964
979
  last_row = first_row + len(df) - 1
965
980
  for k in df.columns:
966
981
  if k in numeric_columns:
967
- col = self.get_column_letter(start_col + df.columns.get_loc(k))
968
- total_row.append(f'=SUM({col}{first_row}:{col}{last_row})')
982
+ col = self.get_column_letter(
983
+ start_col + df.columns.get_loc(k)
984
+ )
985
+ total_row.append(
986
+ f'=SUM({col}{first_row}:{col}{last_row})'
987
+ )
969
988
  else:
970
989
  total_row.append(None)
971
990
 
@@ -973,8 +992,10 @@ class ExcelFile(FileBase):
973
992
  total_row_format = []
974
993
  for k in df.columns:
975
994
  fmt = ['bold','top']
976
- if total_column and k == total_column_name: fmt.append('left')
977
- if k in data_format: fmt.append(data_format[k])
995
+ if total_column and k == total_column_name:
996
+ fmt.append('left')
997
+ if k in data_format:
998
+ fmt.append(data_format[k])
978
999
  total_row_format.append(fmt)
979
1000
 
980
1001
  if total_row_format is None and data_format is not None:
@@ -1044,20 +1065,22 @@ class ExcelFile(FileBase):
1044
1065
  Parameters
1045
1066
  ------------
1046
1067
  start_cell : str | tuple | list
1047
- Individual cell range to be used as the starting position for the fill.
1048
- May be a standard excel range (e.g. 'A2') or a tuple of length two
1049
- (column, row) (e.g. (1, 2) | ('A', 2)). Similar to the formula argument,
1050
- column names may include placeholders (e.g. '{Price}2' | ('{Price}', 2)).
1068
+ Individual cell range to be used as the starting position for the
1069
+ fill. May be a standard excel range (e.g. 'A2') or a tuple of
1070
+ length two (column, row) (e.g. (1, 2) | ('A', 2)). Similar to the
1071
+ formula argument, column names may include placeholders (e.g.
1072
+ '{Price}2' | ('{Price}', 2)).
1051
1073
  formula : str
1052
- Excel formula to be written to the start cell and used as a fill template.
1053
- Column names may include placeholders with header names if 'headers'
1054
- argument is passed (e.g. '=A1+B1-{Price}1'. Placeholders make the formula
1055
- robust to changes in header positioning.
1074
+ Excel formula to be written to the start cell and used as a fill
1075
+ template. Column names may include placeholders with header names
1076
+ if 'headers' argument is passed (e.g. '=A1+B1-{Price}1'.
1077
+ Placeholders make the formula robust to changes in header
1078
+ positioning.
1056
1079
  limit : int
1057
1080
  Number of rows or columns to fill.
1058
1081
  headers : list
1059
- List of column header names. Required argument when placeholders are
1060
- used in formula or start_cell.
1082
+ List of column header names. Required argument when placeholders
1083
+ are used in formula or start_cell.
1061
1084
  formatting : str
1062
1085
  see write() argument of the same name.
1063
1086
  down : bool
@@ -1086,7 +1109,8 @@ class ExcelFile(FileBase):
1086
1109
 
1087
1110
  components = list(set(re.findall('([a-zA-Z]+)(\d+)', formula)))
1088
1111
  cols, rows = zip(*components)
1089
- cols, rows = [self.get_column_index(x) + 1 for x in cols], [int(x) for x in rows]
1112
+ cols = [self.get_column_index(x) + 1 for x in cols]
1113
+ rows = [int(x) for x in rows]
1090
1114
 
1091
1115
  build_counter = lambda x: \
1092
1116
  OrderedDict((i, k) for i, k in enumerate(sorted(list(set(x)))))
@@ -1104,7 +1128,10 @@ class ExcelFile(FileBase):
1104
1128
  for x in range(limit):
1105
1129
  format_args = list(counter.values())
1106
1130
  if not down:
1107
- format_args = [self.get_column_letter(c - 1) for c in format_args]
1131
+ format_args = [
1132
+ self.get_column_letter(c - 1)
1133
+ for c in format_args
1134
+ ]
1108
1135
  data.append(formula.format(*format_args))
1109
1136
  for k in counter:
1110
1137
  counter[k] += 1
@@ -1130,19 +1157,17 @@ class ExcelFile(FileBase):
1130
1157
  ------------
1131
1158
  name : str | tuple | list
1132
1159
  • str ➜ name of new format.
1133
- If 'fmt' is None, if the name is already in self.formats
1134
- then no action is taken. If the name does not already
1135
- exist then the name's components (delimited by underscores
1136
- ('_')) will be combined into a new format (e.g.
1137
- 'bold_commas').
1138
- if 'fmt' is not None, if the name conflicts with an existing
1139
- format name, the existing format will be overwritten.
1140
- tuple | list If 'fmt' is None, components will be combined into a
1141
- new format. For example, self.add_format(
1142
- name=['bold','commas'], fmt=None) would add the
1143
- following entry to self.formats: {'bold_commas':
1144
- {'bold': True, 'num_format': '#,##0'}
1145
-
1160
+ If 'fmt' is None, if the name is already in self.formats then
1161
+ no action is taken. If the name does not already exist then
1162
+ the name's components (delimited by underscores ('_')) will
1163
+ be combined into a new format (e.g.'bold_commas').
1164
+ if 'fmt' is not None, if the name conflicts with an existing
1165
+ format name, the existing format will be overwritten.
1166
+ tuple | list If 'fmt' is None, components will be combined
1167
+ into a new format. For example, self.add_format(name=
1168
+ ['bold','commas'], fmt=None) would add the following entry to
1169
+ self.formats: {'bold_commas': {'bold': True, 'num_format':
1170
+ '#,##0'}
1146
1171
  fmt : dict | None
1147
1172
  • dict ➜ see https://xlsxwriter.readthedocs.io/format.html
1148
1173
  • None ➜ format will be constructed based on the components
@@ -1150,23 +1175,29 @@ class ExcelFile(FileBase):
1150
1175
  Returns
1151
1176
  ------------
1152
1177
  out : str
1153
- name of format (in other words, the self.formats dictionary key value)
1178
+ Name of format (in other words, the self.formats dictionary key
1179
+ value)
1154
1180
  '''
1155
1181
  if fmt is None:
1156
1182
  if isinstance(name, str):
1157
- if name in self.formats: return name
1183
+ if name in self.formats:
1184
+ return name
1158
1185
  name = name.split('_')
1159
- if not isinstance(name, (tuple, list)):
1160
- raise TypeError(
1161
- f"'name' arguments of type {type(name).__name__} not supported."
1162
- )
1186
+
1187
+ odd.validate_value(
1188
+ value=name,
1189
+ name=name,
1190
+ types=(tuple, list)
1191
+ )
1192
+
1163
1193
  fmt = dict()
1164
1194
  for k in name: fmt.update(self.formats[k])
1165
1195
  name = '_'.join(odd.natural_sort(name))
1166
1196
  else:
1167
1197
  if not isinstance(name, str):
1168
1198
  raise TypeError(
1169
- "'name' argument must be a string if 'fmt' is not None, not: "
1199
+ "'name' argument must be a string "
1200
+ "if 'fmt' is not None, not: "
1170
1201
  f"{type(fmt).__name__}"
1171
1202
  )
1172
1203
 
@@ -35,8 +35,8 @@ class SQLiteFile(FileBase):
35
35
  def update_query(tbl_name, update_cols, where_cols, where_logic=''):
36
36
  sql = 'UPDATE {0} SET {1} WHERE {2} {3}'.format(
37
37
  tbl_name,
38
- ', '.join('[%s] = ?' % x for x in odd.to_iter(update_cols)),
39
- ' AND '.join('[%s] = ?' % x for x in odd.to_iter(where_cols)),
38
+ ', '.join('[%s] = ?' % x for x in odd.ensure_list(update_cols)),
39
+ ' AND '.join('[%s] = ?' % x for x in odd.ensure_list(where_cols)),
40
40
  where_logic
41
41
  )
42
42
  return sql
@@ -46,8 +46,8 @@ class SQLiteFile(FileBase):
46
46
  def select_query(tbl_name, select_cols, where_cols, where_logic=''):
47
47
  sql = 'SELECT {1} FROM {0} WHERE {2} {3}'.format(
48
48
  tbl_name,
49
- ', '.join('[%s]' % x for x in odd.to_iter(select_cols)),
50
- ' AND '.join('[%s] = ?' % x for x in odd.to_iter(where_cols)),
49
+ ', '.join('[%s]' % x for x in odd.ensure_list(select_cols)),
50
+ ' AND '.join('[%s] = ?' % x for x in odd.ensure_list(where_cols)),
51
51
  where_logic
52
52
  )
53
53
  return sql
@@ -57,7 +57,7 @@ class SQLiteFile(FileBase):
57
57
  def delete_query(tbl_name, where_cols, where_logic=''):
58
58
  sql = 'DELETE FROM {0} WHERE {1} {2}'.format(
59
59
  tbl_name,
60
- ' AND '.join('[%s] = ?' % x for x in odd.to_iter(where_cols)),
60
+ ' AND '.join('[%s] = ?' % x for x in odd.ensure_list(where_cols)),
61
61
  where_logic
62
62
  )
63
63
  return sql
@@ -151,8 +151,11 @@ class SQLiteFile(FileBase):
151
151
  def format_payload(self, tbl_name, col_names, payload):
152
152
  if isinstance(payload, tuple): payload = [payload]
153
153
  mapping = self.data_types(tbl_name)
154
- payload = [tuple([mapping[col](cell) if pd.notnull(cell) else None
155
- for col,cell in zip(col_names, row)]) for row in payload]
154
+ payload = [
155
+ tuple([mapping[col](cell) if pd.notnull(cell) else None
156
+ for col, cell in zip(col_names, row)])
157
+ for row in payload
158
+ ]
156
159
  return payload
157
160
 
158
161
 
@@ -187,7 +190,7 @@ class SQLiteFile(FileBase):
187
190
  df.reset_index(inplace=True)
188
191
 
189
192
  df.rename(columns={x: x.lower() for x in df.columns}, inplace=True)
190
- col_names = [x for x in odd.lower_iter(self.columns(tbl_name))
193
+ col_names = [x for x in odd.lower_iterable(self.columns(tbl_name))
191
194
  if x in set(list(df.columns))]
192
195
  df = df[col_names]
193
196
 
@@ -197,9 +200,14 @@ class SQLiteFile(FileBase):
197
200
  )
198
201
 
199
202
  if where_cols:
200
- where_cols = odd.lower_iter(odd.to_iter(where_cols))
203
+ where_cols = odd.lower_iterable(odd.ensure_list(where_cols))
201
204
  col_names = [x for x in col_names if x not in where_cols]
202
- sql = self.update_query(tbl_name, col_names, where_cols, where_logic)
205
+ sql = self.update_query(
206
+ tbl_name,
207
+ col_names,
208
+ where_cols,
209
+ where_logic
210
+ )
203
211
  col_names.extend(where_cols)
204
212
  df = df[col_names]
205
213
  else:
@@ -216,12 +224,17 @@ class SQLiteFile(FileBase):
216
224
  def copy_as_temp(self, target_name, temp_name=None, index=None):
217
225
  ''' creates temporary table based on permanent table '''
218
226
  temp_name = temp_name or target_name
219
- sql = f'CREATE TEMP TABLE {temp_name} AS SELECT * FROM {target_name} LIMIT 0;'
227
+
228
+ sql = f'CREATE TEMP TABLE {temp_name} AS ' + \
229
+ f'SELECT * FROM {target_name} LIMIT 0;'
230
+
220
231
  self.c.execute(sql)
221
232
  self.conn.commit()
222
233
 
223
234
  if index:
224
- sql = f"CREATE INDEX temp.idx ON {temp_name}({', '.join(odd.to_iter(index))})"
235
+ sql = f"CREATE INDEX temp.idx ON {temp_name}(%s)" % \
236
+ ', '.join(odd.ensure_list(index))
237
+
225
238
  self.c.execute(sql)
226
239
  self.conn.commit()
227
240
 
@@ -48,24 +48,27 @@ class ZipFile(FileBase):
48
48
  Parameters
49
49
  ------------
50
50
  payload : str | list | tuple | FileBase | Folder
51
- str representing a single file or folder or list or tuple comprised
52
- of str, FileBase, or Folder representing files and folders to be
53
- zipped
51
+ String representing a single file or folder or a list or tuple
52
+ comprised of string, FileBase, or Folder objects representing
53
+ files and folders to be zipped.
54
54
  delete_original : bool
55
55
  If True, zipped files/folders are deleted after being zipped
56
56
  filter_func : callable
57
- function applied to every file in the folder (argument will be of
58
- type FileBase). If filter_func returns True for a given file it will
59
- be included in the zip process otherwise excluded (e.g. filters out
60
- files that are not of file extention .py lambda x: x.ext == 'py').
57
+ Function applied to every file in the folder (argument will be of
58
+ type FileBase). If filter_func returns True for a given file it
59
+ will be included in the zip process otherwise excluded (e.g.
60
+ filters out files that are not of file extention .py lambda x:
61
+ x.ext == 'py').
61
62
  include_folders : bool
62
- if True, each object being zipped will be placed in a folder
63
- mirroring the name of the folder in which the unzipped version resides.
64
- This argument cannot be true when files_only is True.
63
+ If True, each object being zipped will be placed in a folder
64
+ mirroring the name of the folder in which the unzipped version
65
+ resides.
66
+ Note: This argument cannot be true when files_only is True.
65
67
  files_only : bool
66
- if True, zip folder will only include individual files, folders are
67
- disregarded. For example, if you pass a folder that contains subfolders,
68
- all the individual files will be zipped and subfolders are discarded.
68
+ if True, zip folder will only include individual files, folders
69
+ are disregarded. For example, if you pass a folder that contains
70
+ subfolders, all the individual files will be zipped and subfolders
71
+ are discarded.
69
72
  This is the default behavior when 'payload' contains files. This
70
73
  argument cannot be True when include_folder is True.
71
74
  verbose : bool
@@ -83,14 +86,19 @@ class ZipFile(FileBase):
83
86
  'C:/Folder C/'
84
87
  ]
85
88
 
86
- 'Folder C' contains 'File 3.txt' and a subfolder 'Folder D' with a file called
87
- 'File 4.txt'.
89
+ 'Folder C' contains 'File 3.txt' and a subfolder 'Folder D' with a
90
+ file called 'File 4.txt'.
88
91
 
89
92
  • Default (include_folders=False, files_only=False)
90
- Under default behavior the only time a folder will be included in the zip
91
- folder is when a passed folder contains subfolders
93
+ Under default behavior the only time a folder will be included in
94
+ the zip folder is when a passed folder contains subfolders
92
95
 
93
- Output: ['File 1.txt', 'File 2.txt', 'File 3.txt', 'Folder D/File 4.txt']
96
+ Output: [
97
+ 'File 1.txt',
98
+ 'File 2.txt',
99
+ 'File 3.txt',
100
+ 'Folder D/File 4.txt'
101
+ ]
94
102
 
95
103
  • include_folders=True
96
104
  Every file will retain the folder in which it resides
@@ -155,7 +163,8 @@ class ZipFile(FileBase):
155
163
 
156
164
  if include_folders and files_only:
157
165
  raise ValueError(
158
- "'include_folders' and 'files_only' arguments cannot both be True"
166
+ "'include_folders' and 'files_only' "
167
+ "arguments cannot both be True."
159
168
  )
160
169
 
161
170
  if not isinstance(payload, (list, tuple)):
@@ -174,8 +183,14 @@ class ZipFile(FileBase):
174
183
  folder_can_be_deleted = True
175
184
 
176
185
  for f in obj.walk():
177
- zipped = zip_file(f, f.directory[:-1].replace(obj.parent.path, '').split('/'))
178
- if not zipped: folder_can_be_deleted = False
186
+ p = f.directory[:-1]\
187
+ .replace(obj.parent.path, '')\
188
+ .split('/')
189
+
190
+ zipped = zip_file(f, p)
191
+
192
+ if not zipped:
193
+ folder_can_be_deleted = False
179
194
 
180
195
  if delete_original and folder_can_be_deleted:
181
196
  shutil.rmtree(obj.path)
@@ -383,7 +383,9 @@ class FileBase(object):
383
383
  def trifurcate_and_fill(self, f):
384
384
  ''' trifurcates file and fills gaps with instance attributes '''
385
385
  folder, name, ext = trifurcate(f, default_folder=False)
386
- f = (folder or self.directory) + (name or self.name) + '.' + (ext or self.ext)
386
+ f = (folder or self.directory) + \
387
+ (name or self.name) + '.' + \
388
+ (ext or self.ext)
387
389
  return self.spawn(f)
388
390
 
389
391
 
@@ -447,8 +449,8 @@ class FileBase(object):
447
449
 
448
450
 
449
451
  def swap(self, **kwargs):
450
- ''' quick way of intitializing a new file with different attribute(s)
451
- (e.g. you have a csv file and want an xlsx file of the same name) '''
452
+ ''' returns a new file instance with different attribute(s) (e.g.
453
+ you have a csv file and want an xlsx file of the same name) '''
452
454
  alias_map = {'folder': 'directory', 'ext': 'extension'}
453
455
  kwargs = {alias_map.get(k, k): v for k, v in kwargs.items()}
454
456
  directory, name, extension = [
@@ -32,8 +32,8 @@ def backup_folder(
32
32
  shallow : bool
33
33
  filecmp.cmp(f1, f2, shallow=True) shallow argument.
34
34
  "If shallow is true and the os.stat() signatures (file type, size, and
35
- modification time) of both files are identical, the files are taken to be
36
- equal."
35
+ modification time) of both files are identical, the files are taken to
36
+ be equal."
37
37
  https://docs.python.org/3/library/filecmp.html
38
38
  verbose : bool
39
39
  if True, all folders and files that were backed up or overwritten are
@@ -87,13 +87,15 @@ class Folder(object):
87
87
  '''
88
88
  Description
89
89
  --------------------
90
- Class that enables operations on all contents of a folder collectively,
91
- including obtaining metadata, filtering, deleting, and more. This class
92
- encompasses both files and folders but you can access them separetly by
93
- using the Files and Folders subclasses, respectively.
90
+ Class that enables operations on all contents of a folder
91
+ collectively, including obtaining metadata, filtering, deleting,
92
+ and more. This class encompasses both files and folders but you can
93
+ access them separetly by using the Files and Folders subclasses,
94
+ respectively.
94
95
 
95
- Note: This class and its subclasses are intended to be accessed indirectly
96
- by using the 'contents', 'folders', and 'files' Folder cached properties.
96
+ Note: This class and its subclasses are intended to be accessed
97
+ indirectly by using the 'contents', 'folders', and 'files' Folder
98
+ cached properties.
97
99
 
98
100
  Class Attributes
99
101
  --------------------
@@ -205,7 +207,8 @@ class Folder(object):
205
207
  Example:
206
208
  ------------
207
209
  Consider a folder that contains multiple files that include a
208
- YYYY-MM-DD timestamp in the name such as '2024-06-23 Budget.xlsx'.
210
+ YYYY-MM-DD timestamp in the name such as
211
+ '2024-06-23 Budget.xlsx'.
209
212
  In this case you might pass date_pattern=r'\d{4}\-\d{2}\-\d{2}'
210
213
 
211
214
  date_format : str
@@ -214,35 +217,37 @@ class Folder(object):
214
217
  • 'date_pattern' is None ➜ an attempt will be made to derive
215
218
  the regex pattern using 'date_format' as the template.
216
219
  • 'date_pattern' is not None ➜ 'date_format' is optional but
217
- it can be included if you do not want to rely on pd.to_datetime
218
- inferring the format.
220
+ it can be included if you do not want to rely on
221
+ pd.to_datetime inferring the format.
219
222
 
220
223
  Example:
221
224
  ------------
222
225
  Dealing with the same example from above, all the following
223
226
  combinations will achieve the same result since the None values
224
227
  being inferred:
225
- • date_pattern=r'\d{4}\-\d{2}\-\d{2}', date_format='%Y-%m-%d'
228
+ • date_pattern=r'\d{4}\-\d{2}\-\d{2}',
229
+ date_format='%Y-%m-%d'
226
230
  • date_pattern=None, date_format='%Y-%m-%d'
227
231
  • date_pattern=r'\d{4}\-\d{2}\-\d{2}', date_format=None
228
232
 
229
233
  ext : str
230
234
  If not None, only those file objects with this file extension
231
235
  will be considered candidates. This argument may be passed with
232
- or without a period and is not case sensitive (e.g. '.txt', 'txt',
233
- '.TXT' are all valid).
236
+ or without a period and is not case sensitive (e.g. '.txt',
237
+ 'txt', '.TXT' are all valid).
234
238
  index : int
235
- If not None, the object at this integer index in the filtered and
236
- sorted data is returned.
239
+ If not None, the object at this integer index in the filtered
240
+ and sorted data is returned.
237
241
  sort_by : str
238
- Name of self.meta_data column to sort by. This argument must be None
239
- if the 'date_pattern' or 'date_format' arguments are not None. Defaults
240
- to the object created date if None.
242
+ Name of self.meta_data column to sort by. This argument must
243
+ be None if the 'date_pattern' or 'date_format' arguments are
244
+ not None. Defaults to the object created date if None.
241
245
  ascending : bool
242
- If True, data is sorted in ascending order, otherwise, descending order.
243
- If the 'date_pattern' or 'date_format' arguments are not None, the sort
244
- will occur on the timestamps extracted from the object names, otherwise,
245
- the column denoted by the 'sort_by' argument is used.
246
+ If True, data is sorted in ascending order, otherwise,
247
+ descending order. If the 'date_pattern' or 'date_format'
248
+ arguments are not None, the sort will occur on the timestamps
249
+ extracted from the object names, otherwise, the column denoted
250
+ by the 'sort_by' argument is used.
246
251
  errors : bool
247
252
  determines how exceptions are handled:
248
253
  • 'raise': exception is raised
@@ -278,8 +283,9 @@ class Folder(object):
278
283
  else:
279
284
  if date_format or date_pattern:
280
285
  raise ValueError(
281
- "cannot pass 'sort_by' argument if either 'date_format' "
282
- "or 'date_pattern' arguments are not None."
286
+ "cannot pass 'sort_by' argument if either "
287
+ "'date_format' or 'date_pattern' arguments are "
288
+ "not None."
283
289
  )
284
290
 
285
291
  kind = self.__class__.__name__.lower()
@@ -311,12 +317,15 @@ class Folder(object):
311
317
 
312
318
  if df.empty:
313
319
  if errors == 'ignore': return
314
- message = [f"no {kind} matching name pattern '{name_pattern}'"]
315
- if ext is not None: message.append(f"with extension '.{ext}'")
320
+ message = [
321
+ f"no {kind} matching name pattern '{name_pattern}'"
322
+ ]
323
+ if ext is not None:
324
+ message.append(f"with extension '.{ext}'")
316
325
  raise ValueError(' '.join(message + ['found.']))
317
326
 
318
327
  if date_format is not None and date_pattern is None:
319
- date_pattern = cw.date_format_to_regex(date_format)
328
+ date_pattern = cw.temporal_format_to_regex(date_format)
320
329
 
321
330
  if date_pattern is not None:
322
331
  date_pattern = format_date_pattern(date_pattern)
@@ -333,15 +342,18 @@ class Folder(object):
333
342
  )
334
343
 
335
344
  s = pd.to_datetime(s.dropna(), format=date_format)
336
- df = df.join(s, how='inner').sort_values(by=s.name, ascending=ascending)
345
+
346
+ df = df.join(s, how='inner')\
347
+ .sort_values(by=s.name, ascending=ascending)
337
348
 
338
349
  if index is not None:
339
- if not isinstance(index, int):
340
- raise TypeError(
341
- f"'index' argument must be an integer, not {type(index)}"
342
- )
350
+ odd.validate_value(
351
+ value=index,
352
+ name='index',
353
+ types=int
354
+ )
343
355
  try:
344
- return self.to_dict()[ df.iloc[index].name ]
356
+ return self.to_dict()[df.iloc[index].name]
345
357
  except Exception as error:
346
358
  if errors == 'ignore': return
347
359
  raise error
@@ -682,7 +694,8 @@ class Folder(object):
682
694
  self._subfolder_cache[key] = folder
683
695
  return self._subfolder_cache[key]
684
696
 
685
- # cache subfolder that does not exist yet (it will now if read_only=False)
697
+ # cache subfolder that does not exist yet
698
+ # (it will now if read_only=False)
686
699
  if self.troubleshoot:
687
700
  print(f"could not find subfolder = '{name}'")
688
701
 
@@ -746,16 +759,17 @@ class Folder(object):
746
759
 
747
760
  >> C:/Users/Me/MyFolder/A/B/
748
761
 
749
- • Strings preceded by a period prior to the final argument are treated
750
- like dot folders:
762
+ • Strings preceded by a period prior to the final argument are
763
+ treated like dot folders:
751
764
 
752
765
  folder.join('.A','.B','C')
753
766
 
754
767
  >> C:/Users/Me/MyFolder/.A/.B/C/
755
768
 
756
- • Strings preceded by a period that are also the final argument present
757
- a special case. It will be treated as a dot file unless you add a slash
758
- slash to signal you intend for it to be considered a dot folder.
769
+ • Strings preceded by a period that are also the final argument
770
+ present a special case. It will be treated as a dot file unless
771
+ you add a slash to signal you intend for it to be considered a
772
+ dot folder.
759
773
 
760
774
  In this case, .env would be a dot folder subfile in folder B.
761
775
 
@@ -790,7 +804,8 @@ class Folder(object):
790
804
 
791
805
  if is_file(f):
792
806
  if not self.read_only:
793
- # creates the folder(s) in the file path if they do not already exist
807
+ # creates the folder(s) in the file path if they do not
808
+ # already exist
794
809
  self.join(*f.replace(self.path,'').split('/')[:-1])
795
810
  return self.spawn_file(f, **kwargs)
796
811
 
@@ -847,7 +862,11 @@ class Folder(object):
847
862
  ''' verify join args are valid '''
848
863
 
849
864
  error_template = 'Invalid join argument detected at index %d:'
850
- period_rules = 'However, arguments may begin with or contain a single period.'
865
+
866
+ period_rules = (
867
+ 'However, arguments may begin with '
868
+ 'or contain a single period.'
869
+ )
851
870
 
852
871
  for index, arg in enumerate(args):
853
872
  err_msg = error_template % index
@@ -0,0 +1,7 @@
1
+
2
+
3
+ class ReadOnlyError(Exception):
4
+
5
+ def __init__(self, message=None):
6
+ default = 'cannot perform this action in read-only mode'
7
+ super().__init__(message or default)
@@ -41,7 +41,8 @@ def trifurcate(f, default_folder=True):
41
41
  f = f + '/' if explicitly_folder or '.' not in f.split('/')[-1] else f
42
42
  if f.split('/')[0][-4:].lower() == '.com': f = '//' + f
43
43
  f = f.rsplit('/', 1)
44
- folder = f[0] + '/' if len(f) == 2 else (get_cwd() if default_folder else '')
44
+ folder = f[0] + '/' if len(f) == 2 else \
45
+ (get_cwd() if default_folder else '')
45
46
  name, ext = split_extension(f[-1])
46
47
  return folder, name, ext.lower()
47
48
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pathpilot"
3
- version = "0.3.3"
3
+ version = "0.3.4"
4
4
  description = "Library that facilitates file and folder manipulation in Python."
5
5
  authors = ["Zachary Einck <zacharyeinck@gmail.com>"]
6
6
  license = "MIT"
@@ -10,9 +10,9 @@ homepage = "https://github.com/zteinck/pathpilot"
10
10
 
11
11
  [tool.poetry.dependencies]
12
12
  python = "^3.8"
13
- clockwork = ">=0.3.4"
13
+ clockwork = ">=0.4.0"
14
14
  cachegrab = ">=0.2.2"
15
- oddments = ">=0.3.1"
15
+ oddments = ">=0.4.0"
16
16
  pandas = "*"
17
17
  numpy = "*"
18
18
  XlsxWriter = "*"
@@ -1,6 +0,0 @@
1
-
2
-
3
- class ReadOnlyError(Exception):
4
-
5
- def __init__(self, message=None):
6
- super().__init__(message or 'cannot perform this action in read-only mode')
File without changes
File without changes
File without changes