dukit 0.2.2__tar.gz → 0.2.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dukit
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: dukit - data utilities kit for exploring, modifying and visualizing small but messy data using pandas
5
5
  Keywords: pandas,query,utility,data,diff
6
6
  Author: MartinVoelkl94
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dukit"
3
- version = "0.2.2"
3
+ version = "0.2.4"
4
4
  description = "dukit - data utilities kit for exploring, modifying and visualizing small but messy data using pandas"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "MartinVoelkl94", email = "martinv16248@gmail.com" }]
@@ -25,6 +25,7 @@ from .pandas import (
25
25
  collapse,
26
26
  transpose,
27
27
  date_delta,
28
+ date_table,
28
29
  )
29
30
  from .excel import (
30
31
  format,
@@ -78,6 +79,7 @@ __all__ = (
78
79
  'collapse',
79
80
  'transpose',
80
81
  'date_delta',
82
+ 'date_table',
81
83
  'format',
82
84
  'save',
83
85
  'Box',
@@ -1,6 +1,5 @@
1
1
 
2
2
  import pandas as pd
3
- import openpyxl
4
3
  import typing
5
4
  import os
6
5
 
@@ -447,7 +446,6 @@ class Diffs:
447
446
 
448
447
  #format excel file
449
448
  if apply_format:
450
- wb = openpyxl.load_workbook(path)
451
449
 
452
450
  #metadata sheets
453
451
  sheets_hide = []
@@ -462,7 +460,6 @@ class Diffs:
462
460
  sheet=[sheet_info, sheet_summary, sheet_details],
463
461
  hide_sheets=sheets_hide,
464
462
  freeze_panes='B2',
465
- openpyxl_workbook=wb,
466
463
  )
467
464
  msg = 'DEBUG: info, summary, details sheets formatted'
468
465
  log(msg, context, self.verbosity)
@@ -478,7 +475,6 @@ class Diffs:
478
475
  sheet=diff.name,
479
476
  hide_cols=cols_hide,
480
477
  freeze_panes=freeze_panes,
481
- openpyxl_workbook=wb,
482
478
  )
483
479
  msg = f'DEBUG: diff sheet formatted: "{diff.name}"'
484
480
  log(msg, context, self.verbosity)
@@ -19,9 +19,8 @@ def format(
19
19
  align_vertical='top',
20
20
  align_horizontal='left',
21
21
  wrap_text=True,
22
- openpyxl_workbook=None,
23
22
  verbosity=3,
24
- ): #pragma: no cover (does not affect reading of xlsx files)
23
+ ):
25
24
  """
26
25
  applies formatting to an Excel file:
27
26
  - adjust col width to max length of cell content (accounts for linebreaks)
@@ -29,10 +28,8 @@ def format(
29
28
  - hide specified columns
30
29
  """
31
30
 
32
- if openpyxl_workbook:
33
- wb = openpyxl_workbook
34
- else:
35
- wb = openpyxl.load_workbook(path)
31
+ wb = openpyxl.load_workbook(path)
32
+
36
33
  if sheet:
37
34
  sheetnames = list_(sheet)
38
35
  else:
@@ -1,5 +1,6 @@
1
1
 
2
2
  import pandas as pd
3
+ import datetime
3
4
  from .excel import save
4
5
  from .qlang import (
5
6
  q,
@@ -12,6 +13,8 @@ from .pandas import (
12
13
  embed,
13
14
  collapse,
14
15
  transpose,
16
+ date_delta,
17
+ date_table,
15
18
  )
16
19
 
17
20
 
@@ -104,16 +107,14 @@ class DukitAccessor():
104
107
  def collapse(
105
108
  self,
106
109
  on: str,
107
- template='{colname}',
108
- line_start='#',
109
- line_stop='\n',
110
+ template_col='{colname}',
111
+ template_item='#{counter}: {item}\n',
110
112
  ):
111
113
  df_new = collapse(
112
114
  self.df,
113
115
  on=on,
114
- template=template,
115
- line_start=line_start,
116
- line_stop=line_stop,
116
+ template_col=template_col,
117
+ template_item=template_item,
117
118
  )
118
119
  return df_new
119
120
 
@@ -129,6 +130,44 @@ class DukitAccessor():
129
130
  return df_new
130
131
 
131
132
 
133
+ def date_delta(
134
+ self,
135
+ reference_date: str | datetime.date | pd.Timestamp = None,
136
+ reference_col: str = None,
137
+ linebreak: str = '<br>',
138
+ verbosity: int = 3,
139
+ ):
140
+ return date_delta(
141
+ self.df,
142
+ reference_date=reference_date,
143
+ reference_col=reference_col,
144
+ linebreak=linebreak,
145
+ verbosity=verbosity,
146
+ )
147
+
148
+
149
+ def date_table(
150
+ self,
151
+ reference_date: str | datetime.date | pd.Timestamp = None,
152
+ reference_col: str = None,
153
+ uid: str = None,
154
+ upper: int = None,
155
+ lower: int = None,
156
+ linebreak: str = '<br>',
157
+ verbosity: int = 3,
158
+ ):
159
+ return date_table(
160
+ df=self.df,
161
+ reference_date=reference_date,
162
+ reference_col=reference_col,
163
+ uid=uid,
164
+ upper=upper,
165
+ lower=lower,
166
+ linebreak=linebreak,
167
+ verbosity=verbosity,
168
+ )
169
+
170
+
132
171
  def save(
133
172
  self,
134
173
  path: str,
@@ -262,7 +262,7 @@ def fetch(path, before='now', verbosity=3):
262
262
  timestamp = datetime_(timestamp_str)
263
263
  if timestamp < datetime_(cutoff):
264
264
  timestamps[timestamp] = (timestamp_str, extension)
265
- except Exception: #pragma: no cover
265
+ except Exception:
266
266
  pass
267
267
  if len(timestamps) == 0:
268
268
  text = (
@@ -3,7 +3,7 @@ import pandas as pd
3
3
  import numpy as np
4
4
  import datetime
5
5
  import copy
6
- from .utils import log
6
+ from .utils import log, GREEN_LIGHT
7
7
  from .typing import date_
8
8
 
9
9
  def get_df() -> pd.DataFrame:
@@ -446,13 +446,12 @@ def embed(
446
446
  def collapse(
447
447
  df: pd.DataFrame,
448
448
  on: str,
449
- template='{colname}',
450
- line_start='#',
451
- line_stop='\n',
449
+ template_col='{colname}',
450
+ template_item='#{counter}: {item}\n',
452
451
  ):
453
452
  df = df.groupby(on).agg(list)
454
- df = df.map(lambda x: _to_lines(x, line_start, line_stop))
455
- df.columns = [template.format(colname=col) for col in df.columns]
453
+ df = df.map(lambda x: _to_lines(x, template_item))
454
+ df.columns = [template_col.format(colname=col) for col in df.columns]
456
455
  df.insert(0, on, df.index)
457
456
  df.reset_index(drop=True, inplace=True)
458
457
  return df
@@ -460,8 +459,7 @@ def collapse(
460
459
 
461
460
  def _to_lines(
462
461
  x,
463
- line_start='#',
464
- line_stop='\n',
462
+ template_item='#{counter}: {item}\n',
465
463
  ):
466
464
  if not isinstance(x, list):
467
465
  return x
@@ -470,7 +468,7 @@ def _to_lines(
470
468
  else:
471
469
  x_str = ''
472
470
  for i, item in enumerate(x):
473
- x_str += f'{line_start}{i + 1}: {item}{line_stop}'
471
+ x_str += template_item.format(counter=i + 1, item=item)
474
472
  return x_str
475
473
 
476
474
 
@@ -494,24 +492,18 @@ def transpose(
494
492
 
495
493
 
496
494
  def date_delta(
497
- df,
498
- reference_date=None,
499
- reference_col=None,
500
- verbosity=3,
495
+ df: pd.DataFrame,
496
+ reference_date: str | datetime.date | pd.Timestamp = None,
497
+ reference_col: str = None,
498
+ linebreak: str = '\n',
499
+ verbosity: int = 3,
501
500
  ):
502
501
  """
503
502
  Calculates the number of days
504
503
  between a reference date or col
505
- and all other cols in the df.
504
+ and all other date cols in the df.
506
505
  """
507
506
 
508
- df = df.copy().map(date_).convert_dtypes()
509
-
510
- for col in df.columns:
511
- if df[col].isna().all():
512
- df.drop(columns=col, inplace=True)
513
-
514
-
515
507
  if reference_date is None and reference_col is None:
516
508
  msg = 'ERROR: no reference date or column provided'
517
509
  log(msg, 'dk.date_delta', verbosity)
@@ -522,6 +514,27 @@ def date_delta(
522
514
  log(msg, 'dk.date_delta', verbosity)
523
515
  return df
524
516
 
517
+
518
+ df = df.copy()
519
+ df_dates = (
520
+ df
521
+ .copy()
522
+ .map(date_)
523
+ .convert_dtypes()
524
+ )
525
+ cols_dates = []
526
+ cols_all = list(df.columns)
527
+
528
+ for col in df_dates.columns:
529
+ if df_dates[col].isna().all():
530
+ df_dates.drop(columns=col, inplace=True)
531
+ else:
532
+ cols_dates.append(col)
533
+ df.drop(columns=col, inplace=True)
534
+
535
+ df = pd.concat([df, df_dates], axis=1)[cols_all]
536
+
537
+
525
538
  if 'reference_date' in df.columns:
526
539
  msg = 'WARNING: column "reference_date" already exists, overwriting'
527
540
  log(msg, 'dk.date_delta', verbosity)
@@ -531,20 +544,16 @@ def date_delta(
531
544
  else:
532
545
  df['reference_date'] = df[reference_col].apply(date_)
533
546
 
534
- cols_reorder = ['reference_date']
535
- cols_reorder += [
536
- col
537
- for col
538
- in df.columns
539
- if col != 'reference_date'
540
- ]
547
+ cols_reorder = [col for col in df.columns]
541
548
  df = df[cols_reorder]
542
549
 
543
550
  for col in df.columns:
544
- if col == 'reference_date':
551
+ if col not in cols_dates:
552
+ continue
553
+ if col in ['reference_date', reference_col]:
545
554
  continue
546
555
 
547
- name = f'days from<br>{reference_col}<br>to<br>{col}'
556
+ name = f'days from{linebreak}{reference_col}{linebreak}to{linebreak}{col}'
548
557
  if name in df.columns:
549
558
  log(f'WARNING: column "{name}" already exists, overwriting',
550
559
  'dk.date_delta', verbosity)
@@ -554,10 +563,161 @@ def date_delta(
554
563
  #_fix_nas converts accordingly
555
564
  col_formatted = df[col].apply(date_).apply(_fix_nas)
556
565
  col_diff = col_formatted - df['reference_date']
557
- df[name] = col_diff.fillna(pd.NaT).apply(lambda x: x.days)
566
+ df[name] = (
567
+ col_diff
568
+ .fillna(pd.NaT) #type:ignore (pylance doesnt know date_ enables this)
569
+ .apply(lambda x: x.days)
570
+ )
558
571
 
559
572
  return df
560
573
 
561
574
 
562
575
  def _fix_nas(x):
563
576
  return pd.NA if x is pd.NaT else x
577
+
578
+
579
+
580
+
581
+ def date_table(
582
+ df: pd.DataFrame,
583
+ reference_col: str = None,
584
+ uid: str = None,
585
+ upper: int = None,
586
+ lower: int = None,
587
+ start_at_day1: bool = True,
588
+ schedule: dict[int, str] = None,
589
+ linebreak: str = '\n',
590
+ verbosity: int = 3,
591
+ ) -> pd.DataFrame | pd.io.formats.style.Styler:
592
+ """
593
+ Arranges all dates into a table,
594
+ with positions relative to a
595
+ reference date or column.
596
+ """
597
+
598
+ if reference_col is None:
599
+ msg = 'ERROR: no reference column provided'
600
+ log(msg, 'dk.date_delta', verbosity)
601
+ return pd.DataFrame()
602
+
603
+ df = df.copy()
604
+ if uid:
605
+ df.index = df[uid]
606
+ reference = df[reference_col].apply(date_)
607
+
608
+ df = (
609
+ df
610
+ .copy()
611
+ .map(date_)
612
+ .convert_dtypes()
613
+ )
614
+
615
+ for col in df.columns:
616
+ if df[col].isna().all():
617
+ df.drop(columns=col, inplace=True)
618
+
619
+
620
+ blanks = {col: pd.NaT for col in df.columns}
621
+ deltas = pd.DataFrame(
622
+ blanks,
623
+ index=df.index,
624
+ )
625
+
626
+ for col in df.columns:
627
+ #difference only works with pd.NA,
628
+ #but x.days only works with pd.NaT.
629
+ #_fix_nas converts accordingly
630
+ col_formatted = df[col].apply(date_).apply(_fix_nas)
631
+ col_diff = col_formatted - reference
632
+ deltas[col] = (
633
+ col_diff
634
+ .fillna(pd.NaT) #type:ignore (pylance doesnt know date_ enables this)
635
+ .apply(lambda x: x.days)
636
+ )
637
+
638
+ deltas = deltas.T
639
+
640
+
641
+ if upper is None:
642
+ upper = int(deltas.max().max()) + 1
643
+ if lower is None:
644
+ lower = int(deltas.min().min())
645
+
646
+ days = list(range(lower, upper + 1))
647
+ if start_at_day1:
648
+ days.remove(0)
649
+ else:
650
+ days.pop(-1)
651
+ days_df = pd.DataFrame({'days': days})
652
+
653
+ df_timeline = days_df.copy()
654
+ if schedule is not None:
655
+ df_schedule = pd.DataFrame({
656
+ 'days': list(schedule.keys()),
657
+ 'planned': list(schedule.values()),
658
+ })
659
+ df_timeline = df_timeline.merge(
660
+ df_schedule,
661
+ on='days',
662
+ how='left',
663
+ ).fillna('')
664
+
665
+ for col in deltas.columns:
666
+ events = pd.DataFrame({
667
+ 'days': deltas[col].astype('Int64'),
668
+ col: deltas.index,
669
+ })
670
+
671
+ if start_at_day1:
672
+ events = _shift_day0(events)
673
+
674
+ merged = days_df.copy().merge(events, on='days', how='left').fillna('')
675
+ collapsed = collapse(
676
+ merged,
677
+ on='days',
678
+ template_item='{item}' + linebreak,
679
+ )
680
+ df_timeline = df_timeline.merge(
681
+ collapsed,
682
+ on='days',
683
+ how='left',
684
+ )
685
+
686
+ if schedule is not None:
687
+ df_timeline = _highlight_schedule(df_timeline, schedule)
688
+
689
+ return df_timeline
690
+
691
+
692
+ def _shift_day0(df: pd.DataFrame) -> pd.DataFrame:
693
+ if 'days' not in df.columns:
694
+ return df
695
+ if 0 not in df['days'].values:
696
+ return df
697
+ mask_positive = df['days'] >= 0
698
+ df.loc[mask_positive, 'days'] += 1
699
+ return df
700
+
701
+
702
+ def _highlight_schedule(
703
+ df: pd.DataFrame,
704
+ schedule: dict,
705
+ ) -> pd.io.formats.style.Styler:
706
+
707
+ days_highlight = list(schedule.keys())
708
+
709
+ df_style = pd.DataFrame(
710
+ 'text-align: left;',
711
+ columns=df.columns,
712
+ index=df.index
713
+ )
714
+
715
+ mask_highlight = df['days'].isin(days_highlight)
716
+ df_style.loc[mask_highlight, :] += f'background-color: {GREEN_LIGHT};'
717
+
718
+ df_styled = df.style.apply(
719
+ lambda x: df_style,
720
+ axis=None,
721
+ )
722
+
723
+ return df_styled
@@ -4015,6 +4015,7 @@ class SetTypeInfo(Symbol):
4015
4015
  category = 'setter'
4016
4016
  regex = (
4017
4017
  r'\.typeinfo',
4018
+ r'\.type',
4018
4019
  )
4019
4020
 
4020
4021
  #used to validate the current op
@@ -6075,8 +6076,20 @@ class ViewParserHelp(Symbol):
6075
6076
  )
6076
6077
  print('\n\n')
6077
6078
 
6079
+
6078
6080
  if not q.op.operator:
6079
6081
 
6082
+ flags = _get_valid_operators(
6083
+ q.op,
6084
+ 'flags',
6085
+ cols_show
6086
+ )
6087
+ _print_or_display(
6088
+ 'flags:',
6089
+ flags[cols_show],
6090
+ )
6091
+ print('\n\n')
6092
+
6080
6093
  generic_ops = _get_valid_operators(
6081
6094
  q.op,
6082
6095
  'generic',
@@ -6657,7 +6670,7 @@ class FlagNegate(Symbol):
6657
6670
  >>> df.dk.qs(r'id %%!>20000')
6658
6671
  """
6659
6672
  name = 'FlagNegate'
6660
- category = 'syntax'
6673
+ category = 'flag'
6661
6674
  regex = (r'!',)
6662
6675
 
6663
6676
  def parse(self, q: Query) -> Query:
@@ -6668,6 +6681,28 @@ class FlagNegate(Symbol):
6668
6681
 
6669
6682
 
6670
6683
 
6684
+ class FlagIndex(Symbol):
6685
+ """
6686
+ Condition is applied to the index
6687
+ instead of the values.
6688
+
6689
+ Examples
6690
+ --------
6691
+ >>> df.dk.qs(r'%§0')
6692
+ >>> df.dk.qs(r'name §3')
6693
+ """
6694
+ name = 'FlagIndex'
6695
+ category = 'flag'
6696
+ regex = (r'§',)
6697
+
6698
+ def parse(self, q: Query) -> Query:
6699
+ q = _preparse_for_getter(q)
6700
+ q.op.flags['index'] = 'apply condition to the index'
6701
+ return q
6702
+
6703
+
6704
+
6705
+
6671
6706
  class FlagColref(Symbol):
6672
6707
  """
6673
6708
  interpret args as col references
@@ -6678,7 +6713,7 @@ class FlagColref(Symbol):
6678
6713
  >>> df.dk.qs(r'age <@height')
6679
6714
  """
6680
6715
  name = 'FlagColref'
6681
- category = 'syntax'
6716
+ category = 'flag'
6682
6717
  regex = (r'@',)
6683
6718
 
6684
6719
  def parse(self, q: Query) -> Query:
@@ -6706,7 +6741,7 @@ class Flag(Symbol):
6706
6741
  >>> df.dk.qs(r'age %%>30 +strict')
6707
6742
  """
6708
6743
  name = 'Flag'
6709
- category = 'syntax'
6744
+ category = 'flag'
6710
6745
  regex = (r'\+\w+',)
6711
6746
 
6712
6747
  def build(self, str_matched: str) -> 'Symbol':
@@ -6735,6 +6770,7 @@ getters = []
6735
6770
  setters = []
6736
6771
  stylers = []
6737
6772
  viewers = []
6773
+ flags = []
6738
6774
  for _obj in list(locals().values()):
6739
6775
  if isinstance(_obj, type) and issubclass(_obj, Symbol):
6740
6776
  if _obj is not Symbol:
@@ -6758,6 +6794,8 @@ for _obj in list(locals().values()):
6758
6794
  elif _obj.category == 'viewer':
6759
6795
  operators.append(_obj())
6760
6796
  viewers.append(_obj())
6797
+ elif _obj.category == 'flag':
6798
+ flags.append(_obj())
6761
6799
 
6762
6800
 
6763
6801
 
@@ -6782,6 +6820,7 @@ def as_df(category='all') -> pd.DataFrame:
6782
6820
  'setter',
6783
6821
  'styler',
6784
6822
  'viewer',
6823
+ 'flag',
6785
6824
  )
6786
6825
  if category in singular:
6787
6826
  category += 's'
@@ -6804,6 +6843,8 @@ def as_df(category='all') -> pd.DataFrame:
6804
6843
  symbols = stylers
6805
6844
  elif category == 'viewers':
6806
6845
  symbols = viewers
6846
+ elif category == 'flags':
6847
+ symbols = flags
6807
6848
  else:
6808
6849
  raise ValueError(f"Unknown category: {category}")
6809
6850
 
@@ -6813,7 +6854,7 @@ def as_df(category='all') -> pd.DataFrame:
6813
6854
  descriptions = [_get_description(d) for d in docs]
6814
6855
  examples = [_get_example(d) for d in docs]
6815
6856
  categories = [s.category for s in symbols]
6816
- flags = ['\n'.join(s.flags) for s in symbols]
6857
+ flags_actual = ['\n'.join(s.flags) for s in symbols]
6817
6858
  flags_allowed = ['\n'.join(s.flags_allowed) for s in symbols]
6818
6859
  scopes_allowed = ['\n'.join(s.scopes_allowed) for s in symbols]
6819
6860
  connectors_allowed = ['\n'.join(s.connectors_allowed) for s in symbols]
@@ -6823,7 +6864,7 @@ def as_df(category='all') -> pd.DataFrame:
6823
6864
  'description': descriptions,
6824
6865
  'example': examples,
6825
6866
  'category': categories,
6826
- 'flags': flags,
6867
+ 'flags': flags_actual,
6827
6868
  'flags_allowed': flags_allowed,
6828
6869
  'scopes_allowed': scopes_allowed,
6829
6870
  'connectors_allowed': connectors_allowed,
@@ -6844,7 +6885,17 @@ def as_df_styled(category='all') -> pd.io.formats.style.Styler:
6844
6885
 
6845
6886
 
6846
6887
  def _get_lexemes(symbol: Symbol) -> str:
6847
- string = '\n'.join(symbol.regex).replace('\\', '')
6888
+ show_raw = [
6889
+ 'Newline',
6890
+ 'Literal',
6891
+ 'Whitespace',
6892
+ 'Comment',
6893
+ 'Flag',
6894
+ ]
6895
+ if symbol.name in show_raw:
6896
+ string = '\n'.join(symbol.regex)
6897
+ else:
6898
+ string = '\n'.join(symbol.regex).replace('\\', '')
6848
6899
  return string
6849
6900
 
6850
6901
 
@@ -300,7 +300,7 @@ def date_(x, errors='coerce', na=pd.NaT) -> typing.Any:
300
300
  return result.date()
301
301
 
302
302
  #raise and coerce are handled in _datetime and should not be reached here
303
- elif errors == 'raise': #pragma: no cover
303
+ elif errors == 'raise':
304
304
  raise ValueError(
305
305
  f'could not convert "{x}" to datetime.\n'
306
306
  'Error handling:\n'
@@ -312,7 +312,7 @@ def date_(x, errors='coerce', na=pd.NaT) -> typing.Any:
312
312
  elif errors == 'ignore':
313
313
  return x
314
314
  elif errors == 'coerce':
315
- return na #pragma: no cover
315
+ return na
316
316
  else:
317
317
  return errors
318
318
 
@@ -164,7 +164,7 @@ def log(
164
164
  logs.append(message)
165
165
 
166
166
  #for jupyter
167
- if get_ipython().__class__.__name__ == 'ZMQInteractiveShell': #pragma: no cover
167
+ if get_ipython().__class__.__name__ == 'ZMQInteractiveShell':
168
168
 
169
169
  #make html friendly
170
170
  message['text'] = (
File without changes
File without changes
File without changes
File without changes
File without changes