-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathapp.py
1538 lines (1371 loc) · 60.7 KB
/
app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/user/bin/env python3
# pylint: disable=trailing-whitespace
# ruff: noqa: F841, ANN101, ANN001, D415, RET505, I001,
"""Module: PyCriteria Command/REPL Terminal app.
Usage: Commands and REPL
-------------------------
- Multi-line level nested commands structure.
- Run - Core/BASE command AND ANCHORED command.
- Load - TOP INTENT, nested under Run
- Views - SUB COMMAND, nested under Load
Switches between sub-views of the data
- ToDo - SUB COMMAND, nested under Load
Switches between sub-views of the todo tasks
- Find - TOP INTENT, nested under Run
- Locate - SUB COMMAND, nested under Find
Locate a record by ID (row)
Future by column value, or row search term
- Edit - TOP INTENT, nested under Run
Core activity/action of the app for user
- AddNote - SUB COMMAND, nested under Edit
- UpdateNote - SUB COMMAND, nested under Edit
- DeleteNote - SUB COMMAND, nested under Edit
- TooggleTodo - SUB COMMAND, nested under Edit
- Exit - TOP INTENT, nested under Run
If Time, merge the Note commands into one command,
and use a flag/choice to switch action
Linting:
-------------------------
- pylint: disable=trailing-whitespace
- ruff: noqa:
I001 unsorted-imports
Import block is un-sorted or un-formatted
F841: unused-variable
Local variable {name} is assigned to but never used
ANN101 missing-type-self
Missing type annotation for {name} in method
ANN101: missing-type-self
Missing type annotation for {name} in method
RET505 superfluous-else-return
Unnecessary {branch} after return statement
- noqa: W293
Critieria:
LO2.2: Clearly separate and identify code written for the application and
the code from external sources (e.g. libraries or tutorials)
LO2.2.3: Clearly separate code from external sources
LO2.2.4: Clearly identify code from external sources
LO6: Use library software for building a graphical user interface,
or command-line interface, or web application, or mathematical software
LO6.1 Implement the use of external Python libraries
LO6.1.1 Implement the use of external Python libraries
where appropriate to provide the functionality that the project requires.
-------------------------
Standard Libraries
:imports: typing.Literal
3rd Paty Imports
:imports: rich
:imports: rich.panel
:imports: rich.table
:imports: click
:imports: click_repl
:imports: pandas
Local Imports
:imports: commands
:imports: controller
:imports: sidecar
Classes
-------------------------
:class: Controller: Controller for the Terminal App.
:class: ColumnSchema: Column Schema DataViews
:class: Headers: Views by Headers selection: READING, LOADING, VIEWS, DISPLAY
:class: RICHStyler: Rich.Style Defintions class and methods for the app.
:class: WebConsole: Console class and methods for WEB versions of the app.
:class: Inner: Inner Terminal layouts, arrangement.Deprecate? # noqa
:class: Display: Shared Mixed resposnibility with CriteriaApp, and Record
Refactor candidates.
Evoling design artefact
:class: Record: Individual Records, and Records Display/Controller/Model:
:class: Editor: C(R)UD Operations, and Editor Controller/Model:
Global Variables:,
-------------------------
:var: Commands: Commands.py - Commands values
:var: Actions: Controller.py - Controller / Hub logic
:var: AppValues: sidecar.py - AppValues
:var: DataControl: Controller.py - DataController for DataModel, shared, alias
:var: Columns: Controller.py - DataModel schema
:var: Head: Controller.py - Header Views for Views filters
:var: Display: Controller.py - Duplicate effort with CriteriaApp, and Record
:var: Webconsole: Controller.py - WebConsole for WEB versions of the app.
:var: utils: sidecar.py - ProgramUtils, miscellaneous functions
:var: view: app.py - Simialr to Head, but for the app.py commands choices.
Possible Duplicate
:var: window: app.py - Window class for Terminal Layouts, Panels, Cards.
:var: App: app.py - Key DataConrtooler/Controller.py
:var: Guard: app.py - Validation of inputs, and guards conditions/tests.
"""
# 1. Std Lib
from typing import Literal
import click # type: ignore
# 2. 3rd Party
import rich
import rich.panel
from click_repl import register_repl # type: ignore
from pandas import pandas as pd, DataFrame, Series # type: ignore
from rich import pretty, print as rprint # type: ignore
from rich.panel import Panel # type: ignore
from rich.table import Table # type: ignore
from commands import Commands
from controller import (Controller, DataController, ColumnSchema, Headers,
Display, WebConsole, configuration, gspread,
Record, Inner, Editor, )
# 3. Local: Note the controller * intentionally imports all from the module
from logging import AppLogger # type: ignore
from sidecar import AppValues, ProgramUtils
# Note: Move third-party import `gspread` into a type-checking block
# Global Modules/Objects
# 1.0 Logging
Logs: AppLogger = AppLogger(httprequest=AppLogger.httpget,
socket=AppLogger.socket)
# 1.1 Commands.py
Commands: Commands = Commands()
# 1.2 Controller.py
Actions: Controller = Controller()
AppValues: AppValues = AppValues()
DataControl: DataController = DataController(Actions.load_wsheet())
Columns: ColumnSchema = ColumnSchema()
Head: Headers = Headers(labels=Columns)
Display: Display = Display()
Webconsole: WebConsole = WebConsole(configuration.Console.WIDTH,
configuration.Console.HEIGHT)
Webconsole.terminal = Inner()
# 1.3 Sidecar.py
utils: ProgramUtils = ProgramUtils()
pretty.install()
class View:
"""View.
:property: View.Load
:property: View.Todo
"""
Load: list[str] = ["Load", "Begins", "Project", "Criteria",
"ToDo", "Reference", "Refresh"]
Todo: list[str] = ["ToDo", "All", "Simple", "Done", "Grade", "Review"]
def __init__(self):
"""Initialize."""
pass
view: View = View()
class Window:
"""Window: Arrange Terminal Layouts, Panels, Cards.
Methods:
--------------------------------------------
:method: Window.sendto
:method: Window.showrecord
:method: Window.printpane
:method: Window.printpanels
:method: Window.showedited
:method: Window.showmodified
"""
def __init__(self):
"""Initialize."""
pass
@staticmethod
def sendto(value, switch: bool) -> Record | None:
"""Switch Record for dual-return statements.
:param value: Record - Individual Record to display
:param switch: bool - Switch to send to the Editor or not.
"""
return value if switch else None
@staticmethod
def showrecord(data: pd.Series | pd.DataFrame,
sendtoeditor: bool = False,
debug: bool = False) -> Record | None:
"""Display Record.
:param data: pd.Series | pd.DataFrame - Individual Record to display
:param sendtoeditor: bool - Switch to send to the Editor or not.
:param debug: bool - Switch to debug mode or not.
:return: Record | None - Individual Record to display or None
"""
if data.empty is False:
# Individual record holds the singular record, handles displays
# Selecting different calls on the individual record yields
# different displays/outputs.
# Only displays a pd.Series, implicitly.
# Not designed for results >1, unless part of a loop.
individual = Record(series=data, source=data)
if debug:
rprint("Debug Mode: Show Record")
rich.inspect(individual)
if individual.card(consolecard=Webconsole.console) is not None:
window.printpanels(record=individual)
else:
click.echo("Displaying Simple Card")
individual.card(consolecard=Webconsole.console)
return Window.sendto(individual, sendtoeditor)
@staticmethod
def printpane(panel: str, printer: rich.Console) -> None:
"""Print Pane.
:param panel: str - Panel to print
:param printer: rich.Console - Console to print to
:return: None
"""
if panel == 'bannerfull':
pane = Panel(
"======================="
"================="
"Displaying Updated Card"
"========================"
"=================")
printer.print(pane)
@staticmethod
def printpanels(record) -> None:
"""Print Panels.
:param record: Record - Individual Record to display
:return: None
"""
if record is not None:
window.printpane(panel='bannerfull',
printer=Webconsole.console)
# Switch to Rich/Terminal display
header = record.header(
consolehead=Webconsole.console,
sendtolayout=True,
gridfit=True)
current = record.editable(
consoleedit=Webconsole.console,
sendtolayout=True)
footer = record.footer(
consolefoot=Webconsole.console, sendtolayout=True)
# Print: the Panel as a Group of Renderables
record.panel(consolepane=Webconsole.console,
renderable=header,
fits=True,
sendtolayout=False)
record.panel(consolepane=Webconsole.console,
renderable=current,
sendtolayout=False)
record.panel(consolepane=Webconsole.console,
renderable=footer,
fits=True,
sendtolayout=False)
@staticmethod
def showedited(editeddata: pd.Series | pd.DataFrame,
sendtoeditor: bool = False,
debug: bool = False) -> Record | None:
"""Display Edited Record.
:param editeddata: pd.Series | pd.DataFrame
- Individual Record to display
:param sendtoeditor: bool - Switch to send to the Editor or not.
:param debug: bool - Switch to debug mode or not.
:return: Record | None - Individual Record to display or None
"""
if debug:
rprint(editeddata)
elif editeddata.empty is False:
individual = Record(source=editeddata)
if individual.card(consolecard=Webconsole.console) is not None:
window.printpanels(record=individual)
else:
individual.card(consolecard=Webconsole.console)
return individual if sendtoeditor else None
@staticmethod
def showmodified(editeddata: pd.Series,
editor: Editor,
commandtype: Literal['insert', 'append', 'clear'],
dataview: Literal['show', 'compare'] = 'show',
debug=False) -> None:
"""Display Modified Record.
:param editeddata: pd.Series - Individual Record to display
:param editor: Editor - Editor to use
:param commandtype: Literal['insert', 'append', 'clear']
- Command type to use
:param dataview: Literal['show', 'compare'] - Data view to use
:param debug: bool - Switch to debug mode or not.
:return: None
"""
# Display single records
if Record.checksingle(editeddata):
if editor.ismodified and editor.lasteditmode == commandtype:
if debug:
click.echo(message="==========Displaying: "
"Changes=========\n")
# 1. Display the Edited record
if dataview == 'show':
window.showedited(editeddata=editeddata, debug=debug)
elif dataview == 'compare':
window.comparedata(editeddata=editeddata,
editor=editor,
debug=False) # noqa
# [DEBUG]
if debug:
click.echo(message="=== [DEBUG] Saving: "
"changes made [DEBUG]==\n")
click.echo(f" Modified: {editor.lastmodified} ")
else:
click.echo(message="No changes made. See above.")
#
if debug:
click.echo(message="=====================================")
# Switch confirmation on a command's type
if commandtype == 'insert':
click.echo(message=("🆕 A new note is now added 🆕 at:" + editor.lastmodified))
elif commandtype == 'append':
click.echo(message="A note is updated at:"
+ editor.lastmodified)
elif commandtype == 'clear':
click.echo(message="A record's is now deleted at:"
+ editor.lastmodified)
click.echo(message="Exiting: Command completed")
else:
click.echo(message="No changes made. Bulk edits not supported.")
return None
@staticmethod
def comparedata(editeddata: pd.Series,
editor: Editor,
debug=False) -> None:
"""Compare Old and New side by side.
:param editeddata: pd.Series - Individual Record to display
:param editor: Editor - Editor to use
:param debug: bool - Switch to debug mode or not.
:return: None
"""
shown: bool = True
if debug:
rprint(editeddata)
rich.inspect(editeddata)
elif editeddata.empty is False and editor.ismodified:
oldrecord: Record = editor.record
newrecord: Record = Record(series=editor.newresultseries)
# 1. Display the Edited recor
left = oldrecord.editable(
consoleedit=Webconsole.console,
sendtolayout=shown,
title="Original Record")
right = newrecord.editable(
consoleedit=Webconsole.console,
sendtolayout=shown,
title="Updated Record")
# Compare Old with New, else just show the new
if left is not None or right is not None:
window.printpane(panel="bannerfull",
printer=Webconsole.console)
header = newrecord.header(
consolehead=Webconsole.console,
sendtolayout=shown,
gridfit=True)
sidebyside = newrecord.comparegrid(
container=Webconsole.table,
left=left,
right=right,
sendtolayout=shown,
fit=True)
footer = newrecord.footer(
consolefoot=Webconsole.console,
expand=False)
newrecord.panel(consolepane=Webconsole.console,
renderable=header,
fits=True,
sendtolayout=False) # noqa
newrecord.panel(consolepane=Webconsole.console,
renderable=sidebyside,
fits=True,
sendtolayout=False) # noqa
newrecord.panel(consolepane=Webconsole.console,
renderable=footer,
fits=True,
sendtolayout=False) # noqa
else:
window.showedited(editeddata=editeddata, debug=debug)
window: Window = Window()
class CriteriaApp:
"""PyCriteria Terminal App.
:property: values: AppValues - App Values
Methods:
:method: get_data - get the remote data, alias for DataController
:method: display_data -
:method: update_appdata -
:method: display_view -
:method: display_todo -
:method: query_data -
:method: search_data -
:method: search_rows -
:method: rows -
:method: index -
:method: value
:method: get_results
"""
values: AppValues
def __init__(self, values: AppValues):
"""Initialize."""
self.values = values
@staticmethod
def crumbs(context: click.Context) -> None:
"""Display the command navigation crumbs.
:param context: click.Context - Click context
:return: None
"""
click.echo(message="Navigation: CLI: > Run > ... "
"[Intent] > [Action]\n")
root: str = "BASE: Run"
crumb: str = context.info_name if context.info_name \
is not None else root # noqa
text: str = (
f'Navigation: CLI: > Run > ... > Load> {crumb.title()}*\n *: '
+ f'You are here: {crumb.title()}\n'
+ 'To go up one or two level, enter, each time: .. \n'
+ 'To Exit: ctrl + d \n')
click.echo(
message=text)
@staticmethod
def get_data() -> pd.DataFrame:
"""Get the dataframe from the context.
:return: pd.DataFrame - Dataframe
"""
wsheet: gspread.Worksheet = Actions.load_wsheet()
dataframe: pd.DataFrame = \
DataControl.load_dataframe_wsheet(wsheet=wsheet)
return dataframe
@staticmethod
def display_data(dataframe: pd.DataFrame) -> None:
"""Display the dataframe.
:param dataframe: pd.DataFrame - Dataframe to display
:return: None
"""
WebConsole.table = Webconsole.set_datatable(dataframe=dataframe)
Display.display_frame(dataframe=dataframe,
consoleholder=Webconsole.console,
consoletable=Webconsole.table,
headerview=Head.OverviewViews)
@staticmethod
def update_appdata(context, dataframe: pd.DataFrame) -> None:
"""Update the app data.
:param context: click.Context - Click context
:param dataframe: pd.DataFrame - Dataframe to update
"""
context.obj = dataframe
DataControl.dataframe = dataframe
@staticmethod
def display_view(dataframe: pd.DataFrame,
viewer: list[str] = None,
label: str = 'Overview') -> None:
"""Display the dataframe.
:param dataframe: pd.DataFrame - Dataframe to display
:param viewer: list[str] - List of views to display
:param label: str - Label to display
:return: None
"""
headers: list[str] = Head.OverviewViews if view is None else viewer
Webconsole.table = Webconsole.configure_table(headers=headers)
Display.display_subframe(dataframe=dataframe,
consoleholder=Webconsole.console,
consoletable=Webconsole.table,
headerview=headers,
viewfilter=label)
click.echo(message='Your data is refreshed/rehydrated')
@staticmethod
def display_todo(dataframe: pd.DataFrame, viewoption: str = 'All') -> None:
"""Display the dataframe by view option.
:param dataframe: pd.DataFrame - Dataframe to display
:param viewoption: str - View option
:return: None
"""
def select_view(viewss: str) -> list[str]:
"""Select the view."""
if viewss.lower() == 'All'.lower():
headers: list[str] = Head.ToDoAllView
elif viewss.lower() == 'Simple'.lower():
headers: list[str] = Head.ToDoDoDView
elif viewss.lower() == 'Done'.lower():
headers: list[str] = Head.ToDoGradeView
elif viewss.lower() == 'Grade'.lower():
headers: list[str] = Head.ToDoReviewView
elif viewss.lower() == 'Review'.lower():
headers: list[str] = Head.ToDoGradeView
else:
headers: list[str] = Head.ProjectView
return headers
Webconsole.table = Webconsole.configure_table(
headers=select_view(viewoption))
Display.display_subframe(dataframe=dataframe,
consoleholder=Webconsole.console,
consoletable=Webconsole.table,
headerview=select_view(viewoption),
viewfilter='ToDo')
@staticmethod
def query_data(dataframe: pd.DataFrame,
querystring: str,
case: bool = False) -> pd.DataFrame:
"""Query the dataframe.
:param dataframe: pd.DataFrame - Dataframe to query
:param querystring: str - Query string
:param case: bool - Case sensitive
:return: pd.DataFrame - Query result
"""
subsetframe: pd.DataFrame = \
dataframe.apply(lambda x: x.astype(str).str.contains(querystring,
case=case))
return subsetframe
@staticmethod
def search_data(frame: pd.DataFrame,
label: str,
searchstr: str, # noqa
case: bool = False, echos: bool = False) \
-> pd.DataFrame: # noqa
"""Search the dataframe.
:param frame: pd.DataFrame - Dataframe to search
:param label: str - Label to search
:param searchstr: str - Search string
:param case: bool - Case sensitive
:param echos: bool - Echo the result
:return: pd.DataFrame - Search result
"""
searchresult: pd.DataFrame = \
frame[frame[label].astype(str).str.contains(searchstr,
case=case)]
if echos:
click.echo(
message=f'The rows with "{searchstr}" in column\'s'
f' "{label}" are:\n {searchresult}')
return searchresult
@staticmethod
def search_rows(frame: pd.DataFrame,
searchterm: str,
exact: bool = False) -> pd.DataFrame | None:
"""Search across all columns for the searches team
:param frame: pd.DataFrame - Dataframe to search
:param searchterm: str - Search term
:param exact: bool - Exact match
:return: pd.DataFrame - Search result
"""
if searchterm is None or isinstance(searchterm, str):
return None
# Search across all columns for the searches text/str value
mask = frame.apply(lambda column: column.astype(str).
str.contains(searchterm))
#
return frame.loc[mask.all(axis=1)] if exact \
else frame.loc[mask.any(axis=1)]
@staticmethod
def rows(frame: pd.DataFrame,
index: int = None,
searchterm: str = None,
strict: bool = False,
zero: bool = True, debug: bool = False) \
-> pd.DataFrame | pd.Series | None:
"""Get the rows from the dataframe.
Parameters
----------
frame: pd.DataFrame: Data to searches by rows
The dataframe to searches.
index: int: optional
The index to searches for, by default None
searchterm: str: optional
The searches term to searches for, by default None
strict: bool: optional
Whether to searches for
- a non-exact (any) match, by default False, so any can match
- exact (all), by True, so all muct match
zero: bool: optional
Whether to searches for a zero indexed dataset, by default True
debug: bool: optional debug flag, by default False
return pd.DataFrame | None: - Expect a result or None
"""
result: pd.DataFrame | pd.Series | None
if index:
result = App.index(frame=frame, index=index, zero=zero) # noqa
elif searchterm:
# Search across all columns for the position value
result = App.search_rows(frame=frame,
searchterm=searchterm,
exact=strict)
if result.empty is False:
click.echo(f"Could not find {searchterm}")
elif debug:
click.echo(f"Found: {result}")
else:
click.echo("Please provide either "
"an index or searches term")
return None
return result
@staticmethod
def index(frame: pd.DataFrame,
index: int = None,
zero: bool = True, debug: bool = False) \
-> pd.DataFrame | pd.Series | None:
"""Get the index from the dataframe.
:param frame: pd.DataFrame - Dataframe to search
:param index: int - Index to search
:param zero: bool - Zero index
:param debug: bool - Debug flag
:return: pd.DataFrame - Index result
"""
if zero and index is not None:
result = frame.iloc[index - 1] \
if index >= 0 else frame.iloc[index]
if debug:
click.echo(f"Found: {result} "
f"for zero index {index - 1}")
return result
elif not zero and index is not None:
result = frame.iloc[index]
if debug:
click.echo(f"Found: {result} "
f"for standard index {index}")
return result
else:
click.echo(f"Could not find index {index}")
return None
@staticmethod
def value(frame: pd.DataFrame | pd.Series,
row: int = None,
column: int | str = None,
direct: bool = False) -> str | None:
"""Get the value from the dataframe.
:param frame: pd.DataFrame - Dataframe to search
:param row: int - Row to search
:param column: int - Column to search
:param direct: bool - Direct search
:return: str - Value result
"""
if row is None or column is None:
click.echo(message="Plese try again: "
f"One of {row}, {column} is blank")
return None
try:
if not direct and isinstance(row, int) and isinstance(column, int):
if row < 0 and column < 0:
raise IndexError
result: str = frame.iat[row, column]
return result
elif direct and isinstance(row, int) and isinstance(column, int):
if row >= 0 or column >= 0:
result: str = frame.iloc[row, column]
return result
else:
raise IndexError
except IndexError:
click.echo("No value found.")
return None
@staticmethod
def get_results(sourceframe: pd.DataFrame,
filterframe: pd.DataFrame,
exact: bool = False,
axes=1) -> pd.DataFrame | None:
"""Get the results from the query.
:param sourceframe: pd.DataFrame - Source dataframe
:param filterframe: pd.DataFrame - Filter dataframe
:param exact: bool - Exact match
:param axes: int - Axis to search
:return: pd.DataFrame - Result dataframe
"""
if not exact:
resultframe: pd.DataFrame | None = \
sourceframe.where(filterframe).dropna(how='any') \
.dropna(how='any', axis=axes) # noqa
else:
resultframe: pd.DataFrame | None = \
sourceframe.where(filterframe).dropna(how='all') \
.dropna(how='all', axis=axes) # noqa
#
return resultframe if not None else filterframe
App: CriteriaApp = CriteriaApp(values=AppValues)
class Checks:
"""Checks.
:meth: isnot_context
:meth: has_dataframe
:meth: isnot_querytype
:meth: is_querycomplete
:meth: compare_frames
:meth: santitise
"""
def __init__(self):
"""Initialize."""
pass
@staticmethod
def isnot_context(ctx: click.Context) -> bool:
"""Tests dataframe context.
:param ctx: click.Context - Click context
:return: bool - True if not context
"""
return not (ctx and isinstance(ctx, click.Context))
@staticmethod
def has_dataframe(ctx: click.Context) -> bool:
"""Tests dataframe context.
Hinted by Sourcery
- Lift code into else after jump in control flow,
- Replace if statement with if expression,
- Swap if/else branches of if expression to remove negation
Refactored from: Perplexity
www.perplexity.ai/searches/a2f9c214-11e8-4f7d-bf67-72bfe08126de?s=c
:param ctx: click.Context - Click context
:return: bool - True if it has a dataframe
"""
return isinstance(ctx, pd.DataFrame) if True else False
@staticmethod
def isnot_querytype(header: str,
query: str) -> bool:
"""Tests Search Query.
:header: str - Header
:query: str - Query
:return: bool - True if not query
"""
return not isinstance(header, str) or not isinstance(query, str)
@staticmethod
def is_querycomplete(header: str,
query: str) -> bool:
"""Tests for Empty Serach.
:header: str - Header
:query: str - Query
:return: bool - True if header or query is not empty
"""
return header is not None or query is not None
@staticmethod
def compare_frames(olddata: pd.DataFrame,
newdata: pd.DataFrame) -> bool:
"""Compare dataframes.
:param olddata: pd.DataFrame - Old dataframe
:param newdata: pd.DataFrame - New dataframe
:return: bool - True if no changes
"""
diff = olddata.compare(newdata,
keep_shape=True,
keep_equal=True)
if not diff.empty:
click.echo(message="Updated refreshed/rehydrated data")
rprint(diff, flush=True)
return False
click.echo(message="No changes in refreshed/rehydrated data")
return True
@staticmethod
def santitise(s: str) -> str | None:
"""Sanitise strings.
:param s: str - String to sanitised
:return: str | None - Sanitised string or None
"""
empty: str = ''
if s != empty and isinstance(s, str):
return s.strip() if s else None
click.echo(message="Searching by index only. No keywords.")
return None
Guard: Checks = Checks()
class Results:
"""Results.
Critical for all index and search results for rows.
:meth: getrowframe: Get a row from a dataframe by an index or a search term.
"""
def __init__(self):
"""Initialize."""
pass
@staticmethod
def getrowframe(data: pd.DataFrame,
ix: int, st: str, debug: bool = False) \
-> pd.Series | pd.DataFrame | None:
"""Get a row from a dataframe by index or searches term.
:param data: pd.DataFrame - Dataframe
:param ix: int - Index
:param st: str - Search term - Not yet implemented, critical to design
:param debug: bool - Debug
:return: pd.Series | pd.DataFrame | None - Row or rows
"""
if ix and not st:
result = App.rows(frame=data, index=ix)
elif ix:
result = App.rows(frame=data, index=ix, searchterm=st)
elif st:
result = App.rows(frame=data, searchterm=st)
else:
click.echo(f"No Data for row: {ix}")
return None
if isinstance(result, pd.Series):
if debug:
click.echo(f"GetRowFrame(): Found a record\n")
rich.inspect(result)
return result
elif isinstance(result, pd.DataFrame):
if debug:
click.echo("GetRowFrame(): Found a set of records")
rich.inspect(result)
return result
else:
click.echo("GetRowFrame(): Found something: undefined")
if debug:
rich.inspect(result)
return None
@click.group(name=AppValues.Run.cmd)
@click.pass_context
def run(ctx: click.Context) -> None: # noqa
"""Level: Run. Type: about to learn to use this CLI.
:param ctx: click.Context
:return: None: Produces stdout --help text
"""
# 1. Load Data: Have the user load the data:
# A) The app pulls the remote Google connection;
# B) Loads Intent intentionally allows the user to load the data.
@run.group(name=AppValues.Load.cmd)
@click.pass_context
def load(ctx: click.Context) -> None: # noqa
"""Load data from the current context, intentionally.
a) Get the dataframe from remote
b) Check context for dataframe if is/is not present
c) Display dataframe accoridngly
:param ctx: click.Context
:return: None: Produces stdout --help text
"""
@load.command("todo", help="Load todo list. Select views to Display.")
@click.pass_context
@click.option('-d', '--Display',
type=str,
default='All',
show_default=True,
help="Choose a Display: from All, Simple, Done, Grade, Review")
@click.option('-s', '--selects',
type=click.Choice(choices=Head.ToDoChoices),
default='All',
show_default=True,
help="Choose a option: from All, Simple, Done, Grade, Review")
def todo(ctx, display: str, selects: str) -> None:
"""Load todos, and Display different filters/views.
:param ctx: click.Context
:param display: str: Views options by text input
:param selects: str: Views options to select by choice
:return: None: Produces stdout --help text
"""
dataframe: pd.DataFrame = App.get_data()
def viewopt(inputs: str, choice: str) -> str:
"""Choose a view input source to Display."""
if inputs == choice:
return choice
elif inputs is not None:
return inputs
elif choice is not None:
return choice
else:
return choice
# Display
try:
App.display_todo(dataframe=dataframe,
viewoption=viewopt(inputs=display, choice=selects))
except TypeError:
App.display_todo(dataframe=dataframe)
rprint("You entered the wrong option: "
f"{display.lower()}\n"
f"Please try again@ All, Simple, Done, Grade or Review") # noqa
finally:
App.update_appdata(context=ctx, dataframe=dataframe)
load.add_command(todo)
@load.command("views", help="Load views. Select bluk data to view to Display.")
@click.option('-d', '--Display',
type=click.Choice(choices=view.Load),
default='All', show_default=True)
@click.pass_context
def views(ctx, display) -> None:
"""Load Reference/Index.
:param ctx: click.Context
:param display: str: Views options by choice options inpyt
:return: None: Produces stdout --help text
"""
dataframe: pd.DataFrame = App.get_data()
if display == 'All':
App.display_view(dataframe=dataframe,
viewer=Head.OverviewViews,
label="Overview") # noqa
elif display == 'Project':
App.display_view(dataframe=dataframe,
viewer=Head.ProjectView,
label="Project")