dukit 0.2.2__tar.gz → 0.2.3__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.3
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.3"
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,7 +19,6 @@ 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
23
  ): #pragma: no cover (does not affect reading of xlsx files)
25
24
  """
@@ -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,
@@ -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)
@@ -541,10 +554,12 @@ def date_delta(
541
554
  df = df[cols_reorder]
542
555
 
543
556
  for col in df.columns:
557
+ if col not in cols_dates:
558
+ continue
544
559
  if col == 'reference_date':
545
560
  continue
546
561
 
547
- name = f'days from<br>{reference_col}<br>to<br>{col}'
562
+ name = f'days from{linebreak}{reference_col}{linebreak}to{linebreak}{col}'
548
563
  if name in df.columns:
549
564
  log(f'WARNING: column "{name}" already exists, overwriting',
550
565
  'dk.date_delta', verbosity)
@@ -554,10 +569,173 @@ def date_delta(
554
569
  #_fix_nas converts accordingly
555
570
  col_formatted = df[col].apply(date_).apply(_fix_nas)
556
571
  col_diff = col_formatted - df['reference_date']
557
- df[name] = col_diff.fillna(pd.NaT).apply(lambda x: x.days)
572
+ df[name] = (
573
+ col_diff
574
+ .fillna(pd.NaT) #type:ignore (pylance doesnt know date_ enables this)
575
+ .apply(lambda x: x.days)
576
+ )
558
577
 
559
578
  return df
560
579
 
561
580
 
562
581
  def _fix_nas(x):
563
582
  return pd.NA if x is pd.NaT else x
583
+
584
+
585
+
586
+
587
+ def date_table(
588
+ df: pd.DataFrame,
589
+ reference_date: str | datetime.date | pd.Timestamp = None,
590
+ reference_col: str = None,
591
+ uid: str = None,
592
+ upper: int = None,
593
+ lower: int = None,
594
+ start_at_day1: bool = True,
595
+ schedule: dict = None,
596
+ linebreak: str = '\n',
597
+ verbosity: int = 3,
598
+ ) -> pd.DataFrame | pd.io.formats.style.Styler:
599
+ """
600
+ Arranges all dates into a table,
601
+ with positions relative to a
602
+ reference date or column.
603
+ """
604
+
605
+ if reference_date is None and reference_col is None:
606
+ msg = 'ERROR: no reference date or column provided'
607
+ log(msg, 'dk.date_delta', verbosity)
608
+ return df
609
+
610
+ if reference_date is not None and reference_col is not None:
611
+ msg = 'ERROR: both reference date and column provided'
612
+ log(msg, 'dk.date_delta', verbosity)
613
+ return df
614
+
615
+ df = df.copy()
616
+
617
+ if uid:
618
+ df.index = df[uid]
619
+
620
+ if reference_date is not None:
621
+ reference = pd.Series(
622
+ date_(reference_date),
623
+ index=df.index
624
+ )
625
+ else:
626
+ reference = df[reference_col].apply(date_)
627
+
628
+ df = (
629
+ df
630
+ .copy()
631
+ .map(date_)
632
+ .convert_dtypes()
633
+ )
634
+
635
+ for col in df.columns:
636
+ if df[col].isna().all():
637
+ df.drop(columns=col, inplace=True)
638
+
639
+
640
+ blanks = {col: pd.NaT for col in df.columns}
641
+ deltas = pd.DataFrame(
642
+ blanks,
643
+ index=df.index,
644
+ )
645
+
646
+ for col in df.columns:
647
+ #difference only works with pd.NA,
648
+ #but x.days only works with pd.NaT.
649
+ #_fix_nas converts accordingly
650
+ col_formatted = df[col].apply(date_).apply(_fix_nas)
651
+ col_diff = col_formatted - reference
652
+ deltas[col] = (
653
+ col_diff
654
+ .fillna(pd.NaT) #type:ignore (pylance doesnt know date_ enables this)
655
+ .apply(lambda x: x.days)
656
+ )
657
+
658
+ deltas = deltas.T
659
+
660
+
661
+ if upper is None:
662
+ upper = int(deltas.max().max())
663
+ if lower is None:
664
+ lower = int(deltas.min().min())
665
+
666
+ days = list(range(lower, upper + 1))
667
+ if start_at_day1:
668
+ days.remove(0)
669
+ days_df = pd.DataFrame({'days': days})
670
+
671
+ df_timeline = days_df.copy()
672
+ if schedule is not None:
673
+ df_schedule = pd.DataFrame({
674
+ 'days': list(schedule.values()),
675
+ 'planned': list(schedule.keys()),
676
+ })
677
+ df_timeline = df_timeline.merge(
678
+ df_schedule,
679
+ on='days',
680
+ how='left',
681
+ ).fillna('')
682
+
683
+ for col in deltas.columns:
684
+ events = pd.DataFrame({
685
+ 'days': deltas[col].astype('Int64'),
686
+ col: deltas.index,
687
+ })
688
+
689
+ if start_at_day1:
690
+ events = _shift_day0(events)
691
+
692
+ merged = days_df.copy().merge(events, on='days', how='left').fillna('')
693
+ collapsed = collapse(
694
+ merged,
695
+ on='days',
696
+ template_item='{item}' + linebreak,
697
+ )
698
+ df_timeline = df_timeline.merge(
699
+ collapsed,
700
+ on='days',
701
+ how='left',
702
+ )
703
+
704
+ if schedule is not None:
705
+ df_timeline = _highlight_schedule(df_timeline, schedule)
706
+
707
+ return df_timeline
708
+
709
+
710
+ def _shift_day0(df: pd.DataFrame) -> pd.DataFrame:
711
+ if 'days' not in df.columns:
712
+ return df
713
+ if 0 not in df['days'].values:
714
+ return df
715
+ mask_positive = df['days'] >= 0
716
+ df.loc[mask_positive, 'days'] += 1
717
+ return df
718
+
719
+
720
+ def _highlight_schedule(
721
+ df: pd.DataFrame,
722
+ schedule: dict,
723
+ ) -> pd.io.formats.style.Styler:
724
+
725
+ days_highlight = list(schedule.values())
726
+
727
+ df_style = pd.DataFrame(
728
+ 'text-align: left;',
729
+ columns=df.columns,
730
+ index=df.index
731
+ )
732
+
733
+ mask_highlight = df['days'].isin(days_highlight)
734
+ df_style.loc[mask_highlight, :] += f'background-color: {GREEN_LIGHT};'
735
+
736
+ df_styled = df.style.apply(
737
+ lambda x: df_style,
738
+ axis=None,
739
+ )
740
+
741
+ return df_styled
@@ -6075,8 +6075,20 @@ class ViewParserHelp(Symbol):
6075
6075
  )
6076
6076
  print('\n\n')
6077
6077
 
6078
+
6078
6079
  if not q.op.operator:
6079
6080
 
6081
+ flags = _get_valid_operators(
6082
+ q.op,
6083
+ 'flags',
6084
+ cols_show
6085
+ )
6086
+ _print_or_display(
6087
+ 'flags:',
6088
+ flags[cols_show],
6089
+ )
6090
+ print('\n\n')
6091
+
6080
6092
  generic_ops = _get_valid_operators(
6081
6093
  q.op,
6082
6094
  'generic',
@@ -6657,7 +6669,7 @@ class FlagNegate(Symbol):
6657
6669
  >>> df.dk.qs(r'id %%!>20000')
6658
6670
  """
6659
6671
  name = 'FlagNegate'
6660
- category = 'syntax'
6672
+ category = 'flag'
6661
6673
  regex = (r'!',)
6662
6674
 
6663
6675
  def parse(self, q: Query) -> Query:
@@ -6668,6 +6680,28 @@ class FlagNegate(Symbol):
6668
6680
 
6669
6681
 
6670
6682
 
6683
+ class FlagIndex(Symbol):
6684
+ """
6685
+ Condition is applied to the index
6686
+ instead of the values.
6687
+
6688
+ Examples
6689
+ --------
6690
+ >>> df.dk.qs(r'%§0')
6691
+ >>> df.dk.qs(r'name §3')
6692
+ """
6693
+ name = 'FlagIndex'
6694
+ category = 'flag'
6695
+ regex = (r'§',)
6696
+
6697
+ def parse(self, q: Query) -> Query:
6698
+ q = _preparse_for_getter(q)
6699
+ q.op.flags['index'] = 'apply condition to the index'
6700
+ return q
6701
+
6702
+
6703
+
6704
+
6671
6705
  class FlagColref(Symbol):
6672
6706
  """
6673
6707
  interpret args as col references
@@ -6678,7 +6712,7 @@ class FlagColref(Symbol):
6678
6712
  >>> df.dk.qs(r'age <@height')
6679
6713
  """
6680
6714
  name = 'FlagColref'
6681
- category = 'syntax'
6715
+ category = 'flag'
6682
6716
  regex = (r'@',)
6683
6717
 
6684
6718
  def parse(self, q: Query) -> Query:
@@ -6706,7 +6740,7 @@ class Flag(Symbol):
6706
6740
  >>> df.dk.qs(r'age %%>30 +strict')
6707
6741
  """
6708
6742
  name = 'Flag'
6709
- category = 'syntax'
6743
+ category = 'flag'
6710
6744
  regex = (r'\+\w+',)
6711
6745
 
6712
6746
  def build(self, str_matched: str) -> 'Symbol':
@@ -6735,6 +6769,7 @@ getters = []
6735
6769
  setters = []
6736
6770
  stylers = []
6737
6771
  viewers = []
6772
+ flags = []
6738
6773
  for _obj in list(locals().values()):
6739
6774
  if isinstance(_obj, type) and issubclass(_obj, Symbol):
6740
6775
  if _obj is not Symbol:
@@ -6758,6 +6793,8 @@ for _obj in list(locals().values()):
6758
6793
  elif _obj.category == 'viewer':
6759
6794
  operators.append(_obj())
6760
6795
  viewers.append(_obj())
6796
+ elif _obj.category == 'flag':
6797
+ flags.append(_obj())
6761
6798
 
6762
6799
 
6763
6800
 
@@ -6782,6 +6819,7 @@ def as_df(category='all') -> pd.DataFrame:
6782
6819
  'setter',
6783
6820
  'styler',
6784
6821
  'viewer',
6822
+ 'flag',
6785
6823
  )
6786
6824
  if category in singular:
6787
6825
  category += 's'
@@ -6804,6 +6842,8 @@ def as_df(category='all') -> pd.DataFrame:
6804
6842
  symbols = stylers
6805
6843
  elif category == 'viewers':
6806
6844
  symbols = viewers
6845
+ elif category == 'flags':
6846
+ symbols = flags
6807
6847
  else:
6808
6848
  raise ValueError(f"Unknown category: {category}")
6809
6849
 
@@ -6813,7 +6853,7 @@ def as_df(category='all') -> pd.DataFrame:
6813
6853
  descriptions = [_get_description(d) for d in docs]
6814
6854
  examples = [_get_example(d) for d in docs]
6815
6855
  categories = [s.category for s in symbols]
6816
- flags = ['\n'.join(s.flags) for s in symbols]
6856
+ flags_actual = ['\n'.join(s.flags) for s in symbols]
6817
6857
  flags_allowed = ['\n'.join(s.flags_allowed) for s in symbols]
6818
6858
  scopes_allowed = ['\n'.join(s.scopes_allowed) for s in symbols]
6819
6859
  connectors_allowed = ['\n'.join(s.connectors_allowed) for s in symbols]
@@ -6823,7 +6863,7 @@ def as_df(category='all') -> pd.DataFrame:
6823
6863
  'description': descriptions,
6824
6864
  'example': examples,
6825
6865
  'category': categories,
6826
- 'flags': flags,
6866
+ 'flags': flags_actual,
6827
6867
  'flags_allowed': flags_allowed,
6828
6868
  'scopes_allowed': scopes_allowed,
6829
6869
  'connectors_allowed': connectors_allowed,
@@ -6844,7 +6884,17 @@ def as_df_styled(category='all') -> pd.io.formats.style.Styler:
6844
6884
 
6845
6885
 
6846
6886
  def _get_lexemes(symbol: Symbol) -> str:
6847
- string = '\n'.join(symbol.regex).replace('\\', '')
6887
+ show_raw = [
6888
+ 'Newline',
6889
+ 'Literal',
6890
+ 'Whitespace',
6891
+ 'Comment',
6892
+ 'Flag',
6893
+ ]
6894
+ if symbol.name in show_raw:
6895
+ string = '\n'.join(symbol.regex)
6896
+ else:
6897
+ string = '\n'.join(symbol.regex).replace('\\', '')
6848
6898
  return string
6849
6899
 
6850
6900
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes