2019-10-14 08:44:00 +00:00
|
|
|
#!/usr/bin/python3
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
import os, sys
|
|
|
|
import configparser
|
|
|
|
from dialog import Dialog
|
|
|
|
from datetime import datetime
|
|
|
|
import subprocess
|
|
|
|
import re
|
2019-10-16 02:24:58 +00:00
|
|
|
import glob
|
|
|
|
import hashlib
|
2019-10-14 08:44:00 +00:00
|
|
|
|
|
|
|
solution_description="Journaling system for daily needs."
|
|
|
|
epilog="Designed and written by Chris Punches.\n SILO GROUP LLC, 2019. ALL RIGHTS RESERVED."
|
|
|
|
|
|
|
|
|
|
|
|
class Config():
|
|
|
|
def get_conf_location(self):
|
|
|
|
home=os.getenv("HOME")
|
|
|
|
return "{}/.silojourn.ini".format( home )
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
self.config = configparser.ConfigParser()
|
|
|
|
self.config.read( self.get_conf_location() )
|
|
|
|
self.journal_path = self.config['filesystem']['journal']
|
|
|
|
self.editor = self.config['tools']['editor']
|
2019-10-16 02:24:58 +00:00
|
|
|
self.tracker = self.config['filesystem']['task_tracker']
|
2019-10-14 08:44:00 +00:00
|
|
|
|
|
|
|
def get_journal_path(self):
|
|
|
|
return self.journal_path
|
|
|
|
|
|
|
|
def get_editor(self):
|
|
|
|
return self.editor
|
|
|
|
|
2019-10-16 02:24:58 +00:00
|
|
|
def get_tracker(self):
|
|
|
|
return self.tracker
|
|
|
|
|
|
|
|
|
|
|
|
class Task():
|
|
|
|
def __init__(self, filename, text, position):
|
|
|
|
self.filename = filename
|
|
|
|
self.topic = Journaler.topic_from_filename( self.filename )
|
|
|
|
self.creation_date = Journaler.date_from_filename(self.filename)
|
|
|
|
self.position = position
|
|
|
|
self.text = text
|
|
|
|
self.hash = self._this_hash()
|
|
|
|
|
|
|
|
def _this_hash(self):
|
|
|
|
return hashlib.md5(
|
|
|
|
"{}:{}:{}".format(
|
|
|
|
self.filename,
|
|
|
|
self.position,
|
|
|
|
self.text
|
|
|
|
).encode('utf-8')
|
|
|
|
).hexdigest()[:8]
|
|
|
|
|
2019-10-14 08:44:00 +00:00
|
|
|
|
|
|
|
class Journaler():
|
|
|
|
def __init__(self, config):
|
|
|
|
self.config = config
|
|
|
|
self.d = Dialog( dialog="dialog", autowidgetsize=True)
|
2019-10-16 02:24:58 +00:00
|
|
|
self.d.set_background_title("SILO JOURNALING SYSTEM")
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def topic_from_filename( filename ):
|
|
|
|
try:
|
|
|
|
topic = re.search( "[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]_(.+?)$", filename ).group(1)
|
|
|
|
except AttributeError:
|
|
|
|
topic = "NO_TOPIC"
|
|
|
|
return topic
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def date_from_filename( filename ):
|
|
|
|
try:
|
|
|
|
date = re.search( "([0-9][0-9][0-9][-9]-[0-9][0-9]-[0-9][0-9])_.+?$", filename ).group(1)
|
|
|
|
except AttributeError:
|
|
|
|
date = "NO_TOPIC"
|
|
|
|
return date
|
2019-10-14 08:44:00 +00:00
|
|
|
|
|
|
|
# generate the filepath for a specific entry
|
|
|
|
def _get_entry_filename(self, topic, date):
|
|
|
|
# TODO add sanity checks
|
|
|
|
return "{0}/{1}_{2}".format( self.config.get_journal_path(), date, topic )
|
|
|
|
|
|
|
|
# open a specific entry for a supplied topic and date
|
|
|
|
def _open_entry(self, topic, date):
|
|
|
|
subprocess.run( [ self.config.get_editor(), self._get_entry_filename( topic, date ) ] )
|
|
|
|
|
|
|
|
# gemerate the current date in a standard format
|
|
|
|
def _get_current_date(self):
|
|
|
|
return datetime.today().strftime('%Y-%m-%d')
|
|
|
|
|
|
|
|
# load all entry metadata
|
|
|
|
def _get_all_entries(self):
|
|
|
|
entry_list = os.listdir( self.config.get_journal_path() )
|
|
|
|
return entry_list
|
|
|
|
|
|
|
|
# get all topics for all dates
|
|
|
|
def _get_all_topics(self):
|
|
|
|
topics = list()
|
|
|
|
for item in self._get_all_entries():
|
|
|
|
try:
|
2019-10-16 02:24:58 +00:00
|
|
|
found = re.search("[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]_(.+?)$", item ).group(1)
|
2019-10-14 08:44:00 +00:00
|
|
|
topics.append( found )
|
|
|
|
except AttributeError:
|
|
|
|
pass
|
|
|
|
return sorted( list( dict.fromkeys( topics ) ) )
|
|
|
|
|
|
|
|
# get all topics for all dates
|
|
|
|
def _get_all_dates(self):
|
|
|
|
dates = list()
|
|
|
|
for item in self._get_all_entries():
|
|
|
|
try:
|
|
|
|
found = re.search( "([0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9])_.+?$", item ).group(1)
|
|
|
|
dates.append( found )
|
|
|
|
except AttributeError:
|
|
|
|
pass
|
|
|
|
return sorted( list( dict.fromkeys( dates ) ) )
|
|
|
|
|
|
|
|
# get all topics associated with a date
|
|
|
|
def _get_topics(self, date):
|
|
|
|
topics = list()
|
|
|
|
|
|
|
|
for item in self._get_all_entries():
|
|
|
|
try:
|
|
|
|
found = re.search("{0}_(.+?)$".format(date), item).group(1)
|
|
|
|
topics.append(found)
|
|
|
|
except AttributeError:
|
|
|
|
pass
|
|
|
|
return sorted( list( dict.fromkeys( topics ) ) )
|
|
|
|
|
|
|
|
# get all dates associated with a topic
|
|
|
|
def _get_dates(self, topic):
|
|
|
|
dates = list()
|
|
|
|
|
|
|
|
for item in self._get_all_entries():
|
|
|
|
try:
|
|
|
|
found = re.search("([0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9])_{0}".format( topic ), item ).group(1)
|
|
|
|
dates.append(found)
|
|
|
|
except AttributeError:
|
|
|
|
pass
|
|
|
|
return sorted( list( dict.fromkeys( dates ) ) )
|
|
|
|
|
|
|
|
# browse topics for a specific date
|
|
|
|
def do_topic_browse_by_date(self, date):
|
|
|
|
print( "topic browse by date: {}".format( date ) )
|
|
|
|
topics = self._get_topics( date )
|
|
|
|
|
|
|
|
menu_choices = list()
|
|
|
|
for i in range(len(topics)):
|
|
|
|
menu_choices.append(
|
|
|
|
( topics[i], "[{}] total entries.".format( len( self._get_dates( topics[i] ) ) ) )
|
|
|
|
)
|
|
|
|
|
|
|
|
code, topic = self.d.menu(
|
|
|
|
"Select a topic for date: {0}".format(date),
|
|
|
|
choices=menu_choices,
|
|
|
|
width=self.d.maxsize()[1],
|
|
|
|
extra_button=True,
|
|
|
|
extra_label="New...",
|
|
|
|
ok_label="Select"
|
|
|
|
)
|
|
|
|
|
|
|
|
if code == self.d.OK:
|
|
|
|
self._open_entry(topic, date)
|
|
|
|
self.do_topic_browse_by_date(date)
|
|
|
|
elif code == self.d.EXTRA:
|
|
|
|
input_code, input_topic = self.d.inputbox( "Creating a new topic for today: {}".format( date ), width=self.d.maxsize()[1] )
|
|
|
|
if input_code == self.d.OK:
|
|
|
|
self.do_new_topic( input_topic )
|
|
|
|
else:
|
|
|
|
self.do_topic_browse_by_date( date )
|
|
|
|
else:
|
|
|
|
self.do_browse_topics()
|
|
|
|
|
|
|
|
# browse dates for a specific topic
|
|
|
|
def do_date_browse_by_topic(self, topic):
|
|
|
|
dates = self._get_dates(topic)
|
|
|
|
|
|
|
|
menu_choices = list()
|
|
|
|
for i in range(len(dates)):
|
|
|
|
menu_choices.append( ( dates[i], "{} total topics.".format(
|
|
|
|
len(
|
|
|
|
self._get_topics( dates[i] )
|
|
|
|
)
|
|
|
|
) ) )
|
|
|
|
|
|
|
|
code, date = self.d.menu(
|
|
|
|
"Select a date for topic: {0}".format( topic ),
|
|
|
|
choices=menu_choices,
|
|
|
|
width=self.d.maxsize()[1],
|
|
|
|
cancel_label="Back",
|
|
|
|
ok_label="Select"
|
|
|
|
)
|
|
|
|
|
|
|
|
if code == self.d.OK:
|
|
|
|
self._open_entry(topic, date)
|
|
|
|
self.do_topic_browse_by_date(date)
|
|
|
|
|
|
|
|
else:
|
|
|
|
self.do_browse_topics()
|
|
|
|
|
2019-10-16 02:24:58 +00:00
|
|
|
# return a list of tasks
|
2019-10-16 04:59:21 +00:00
|
|
|
def _get_todos(self, complete=False):
|
2019-10-16 18:21:55 +00:00
|
|
|
result = list()
|
|
|
|
|
2019-10-16 02:24:58 +00:00
|
|
|
for file in sorted( glob.glob(f"{self.config.get_journal_path()}**/*", recursive=False) ):
|
|
|
|
with open( file ) as _file:
|
|
|
|
for position, line in enumerate(_file.readlines()):
|
|
|
|
m = re.search("^TODO(.+?)$", line.rstrip().lstrip() )
|
|
|
|
if m:
|
|
|
|
task = Task(
|
|
|
|
file,
|
|
|
|
m.group(1).rstrip().lstrip(),
|
|
|
|
position
|
|
|
|
)
|
2019-10-16 04:59:21 +00:00
|
|
|
if complete == True:
|
|
|
|
# we only want completed tasks
|
|
|
|
if self._check_task_completion( task ):
|
2019-10-16 18:21:55 +00:00
|
|
|
result.append(task)
|
2019-10-16 04:59:21 +00:00
|
|
|
else:
|
|
|
|
# we only want incomplete tasks
|
|
|
|
if not self._check_task_completion( task ):
|
2019-10-16 18:21:55 +00:00
|
|
|
result.append(task)
|
|
|
|
|
|
|
|
return sorted( result, key=lambda r: r.topic, reverse=True )
|
2019-10-16 02:24:58 +00:00
|
|
|
|
|
|
|
# check if task is completed
|
|
|
|
def _check_task_completion( self, task ):
|
|
|
|
# check if self.config.tracker file is there
|
|
|
|
# create it if not exists
|
|
|
|
# open self.config.tracker file
|
|
|
|
# iterate through lines
|
|
|
|
# find line that begins with hash
|
2019-10-16 02:43:36 +00:00
|
|
|
if not os.path.exists( self.config.tracker ):
|
|
|
|
f = open(self.config.tracker, 'w+')
|
|
|
|
f.close()
|
|
|
|
|
|
|
|
with open( self.config.tracker, 'r' ) as tracker_file:
|
2019-10-16 02:24:58 +00:00
|
|
|
for line in tracker_file:
|
|
|
|
m = re.search( "^{0}\t.*$".format( task.hash ), line )
|
|
|
|
if m:
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
def _mark_task_complete(self, task):
|
|
|
|
print( "marking task {} complete".format( task.hash ) )
|
|
|
|
with open( self.config.tracker, 'a+' ) as tracker_file:
|
|
|
|
tracker_file.write(
|
|
|
|
"{0}\t{1}:{2}\t{3}\t{4}\n".format(
|
|
|
|
task.hash,
|
|
|
|
task.creation_date,
|
|
|
|
self._get_current_date(),
|
|
|
|
task.topic,
|
|
|
|
task.text
|
|
|
|
)
|
|
|
|
)
|
2019-10-14 08:44:00 +00:00
|
|
|
|
2019-10-16 04:59:21 +00:00
|
|
|
def _mark_task_incomplete(self, task):
|
|
|
|
print( "marking task {} complete".format( task.hash ) )
|
|
|
|
# remove any line that starts with this hash
|
|
|
|
with open( self.config.tracker, 'r' ) as tracker_file_r:
|
|
|
|
lines = tracker_file_r.readlines()
|
|
|
|
with open( self.config.tracker, 'w' ) as tracker_file_w:
|
|
|
|
t = []
|
|
|
|
for line in lines:
|
|
|
|
if not line.startswith(task.hash):
|
|
|
|
tracker_file_w.write(line)
|
|
|
|
|
|
|
|
|
|
|
|
def do_browse_completed_todo(self):
|
|
|
|
choices = list()
|
|
|
|
|
2019-10-16 18:21:55 +00:00
|
|
|
todos = self._get_todos(complete=True)
|
|
|
|
for todo in todos:
|
2019-10-16 04:59:21 +00:00
|
|
|
choices.append(
|
|
|
|
(todo.hash, "({0}) {1}".format(todo.topic, todo.text), False)
|
|
|
|
)
|
|
|
|
|
|
|
|
if len(choices) > 0:
|
|
|
|
code, hashes_marked_incomplete = self.d.checklist(
|
|
|
|
text="Completed Tasks",
|
|
|
|
choices=choices,
|
|
|
|
backtitle="Select tasks to remark as incomplete...",
|
|
|
|
width=self.d.maxsize()[1],
|
|
|
|
ok_label="Re-Open",
|
|
|
|
cancel_label="Pending Tasks",
|
|
|
|
help_button=True,
|
|
|
|
help_label="Journal"
|
|
|
|
)
|
|
|
|
|
|
|
|
if code == self.d.OK:
|
|
|
|
# mark these tasks as open by removing from the file
|
|
|
|
tasks_to_open = list()
|
2019-10-16 18:21:55 +00:00
|
|
|
for task in todos:
|
2019-10-16 04:59:21 +00:00
|
|
|
for item in hashes_marked_incomplete:
|
|
|
|
if todo.hash == item:
|
|
|
|
tasks_to_open.append( task )
|
|
|
|
|
|
|
|
for selection in tasks_to_open:
|
|
|
|
self._mark_task_incomplete( selection )
|
|
|
|
|
|
|
|
if code == self.d.CANCEL:
|
|
|
|
# go to pending tasks view
|
|
|
|
self.do_browse_todo_entries()
|
|
|
|
|
|
|
|
if code == self.d.HELP:
|
|
|
|
# go to journal view
|
|
|
|
self.do_browse_topics()
|
|
|
|
|
|
|
|
else:
|
|
|
|
# no completed tasks, go to incomplete tasks view
|
|
|
|
self.do_browse_todo_entries()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2019-10-14 08:44:00 +00:00
|
|
|
def do_browse_todo_entries(self):
|
2019-10-16 02:24:58 +00:00
|
|
|
choices = list()
|
2019-10-16 18:21:55 +00:00
|
|
|
todos = self._get_todos(complete=False)
|
2019-10-16 02:24:58 +00:00
|
|
|
|
2019-10-16 18:21:55 +00:00
|
|
|
for todo in todos:
|
2019-10-16 02:24:58 +00:00
|
|
|
choices.append(
|
|
|
|
( todo.hash, "({0}) {1}".format(todo.topic, todo.text) , False )
|
|
|
|
)
|
|
|
|
|
2019-10-16 02:43:36 +00:00
|
|
|
if len(choices) > 0:
|
|
|
|
code, hashes_marked_complete = self.d.checklist(
|
2019-10-16 04:59:21 +00:00
|
|
|
text="Incomplete Tasks",
|
2019-10-16 02:43:36 +00:00
|
|
|
choices=choices,
|
|
|
|
backtitle="Select tasks to complete.",
|
|
|
|
width=self.d.maxsize()[1],
|
2019-10-16 04:59:21 +00:00
|
|
|
cancel_label="Journal",
|
|
|
|
ok_label="Complete",
|
|
|
|
extra_button=True,
|
|
|
|
extra_label="Log..."
|
2019-10-16 02:43:36 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
if code == self.d.OK:
|
|
|
|
# it only returns the hashes
|
|
|
|
# we want to create a new list of tasks for tasks that should be completed
|
|
|
|
tasks_to_complete = list()
|
|
|
|
|
|
|
|
# iterate through all tasks so nothing gets missed
|
2019-10-16 18:21:55 +00:00
|
|
|
for todo in todos:
|
2019-10-16 02:43:36 +00:00
|
|
|
# in each iteration we want to iterate through the hashes marked complete and add tasks
|
|
|
|
# with a matching hash to that new list
|
|
|
|
for item in hashes_marked_complete:
|
|
|
|
if todo.hash == item:
|
|
|
|
tasks_to_complete.append(todo)
|
|
|
|
|
|
|
|
for selection in tasks_to_complete:
|
|
|
|
self._mark_task_complete( selection )
|
2019-10-16 04:59:21 +00:00
|
|
|
|
|
|
|
elif code == self.d.CANCEL:
|
|
|
|
self.do_browse_topics()
|
|
|
|
elif code == self.d.EXTRA:
|
|
|
|
self.do_browse_completed_todo()
|
|
|
|
|
2019-10-16 02:43:36 +00:00
|
|
|
else:
|
|
|
|
# no todo items
|
|
|
|
self.do_browse_topics()
|
2019-10-16 02:24:58 +00:00
|
|
|
|
|
|
|
def create_new_topic_ui(self):
|
|
|
|
input_code, input_topic = self.d.inputbox("Creating a new topic for today: {}".format(self._get_current_date()),
|
|
|
|
width=self.d.maxsize()[1])
|
|
|
|
if input_code == self.d.OK:
|
|
|
|
self.do_new_topic(input_topic)
|
|
|
|
self.do_browse_topics()
|
|
|
|
else:
|
|
|
|
self.do_browse_topics()
|
2019-10-14 08:44:00 +00:00
|
|
|
|
|
|
|
# browse topics for all dates
|
|
|
|
def do_browse_topics(self):
|
|
|
|
topics = self._get_all_topics()
|
|
|
|
|
2019-10-16 02:24:58 +00:00
|
|
|
if len(topics) > 0:
|
|
|
|
menu_choices = list()
|
|
|
|
for i in range(len(topics)):
|
|
|
|
menu_choices.append( ( topics[i], "{} total entries.".format( len( self._get_dates( topics[i] ) ) ) ) )
|
|
|
|
|
|
|
|
code, topic = self.d.menu(
|
|
|
|
"Select a topic whose dated entries you want to browse:",
|
|
|
|
choices=menu_choices,
|
|
|
|
width=self.d.maxsize()[1],
|
|
|
|
cancel_label="Exit",
|
|
|
|
ok_label="Select",
|
|
|
|
extra_button=True,
|
2019-10-16 04:59:21 +00:00
|
|
|
extra_label="New topic for today...",
|
|
|
|
help_label="Task Manager",
|
|
|
|
help_button=True
|
2019-10-16 02:24:58 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
if code == self.d.OK:
|
|
|
|
self.do_date_browse_by_topic(topic)
|
|
|
|
elif code == self.d.EXTRA:
|
|
|
|
self.create_new_topic_ui()
|
2019-10-16 04:59:21 +00:00
|
|
|
elif code == self.d.HELP:
|
|
|
|
self.do_browse_todo_entries()
|
2019-10-16 02:24:58 +00:00
|
|
|
else:
|
|
|
|
# no topics yet, create one
|
|
|
|
self.create_new_topic_ui()
|
2019-10-14 08:44:00 +00:00
|
|
|
|
|
|
|
# browse dates for all topics
|
|
|
|
def do_list_dates(self):
|
|
|
|
dates = sorted( self._get_all_dates() )
|
|
|
|
|
|
|
|
menu_choices = list()
|
|
|
|
for i in range( len( dates ) ):
|
|
|
|
menu_choices.append( ( dates[i], "{} total topics.".format( len( self._get_topics( dates[i] ) ) ) ) )
|
|
|
|
|
|
|
|
code, date = self.d.menu(
|
|
|
|
"Select a date to browse topics for that date...",
|
|
|
|
choices=menu_choices,
|
|
|
|
width=self.d.maxsize()[1]
|
|
|
|
)
|
|
|
|
|
|
|
|
if code == self.d.OK:
|
|
|
|
self.do_topic_browse_by_date( date )
|
|
|
|
|
|
|
|
def do_new_topic(self, topic):
|
|
|
|
self._open_entry( topic, self._get_current_date() )
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
parser = argparse.ArgumentParser( description=solution_description, prog="silojourn", epilog=epilog )
|
|
|
|
|
|
|
|
# want these options to be mutually exclusive to cut down on cli complexity
|
|
|
|
|
|
|
|
# should pull up a curses menu list that shows a list of topics for a certain date
|
|
|
|
date_group = parser.add_mutually_exclusive_group()
|
|
|
|
date_group.add_argument( "-D", "--date", help="Browse topics by date." )
|
|
|
|
|
|
|
|
# should pull up a curses menu list that shows a list of dates for a certain topic
|
|
|
|
topic_group = parser.add_mutually_exclusive_group()
|
|
|
|
topic_group.add_argument( "-T", "--topic", help="Browse dates by topic." )
|
|
|
|
|
|
|
|
# should pull up a curses menu that lists all "TO DO" lines in all journal entries in the base.
|
|
|
|
# this menu should be able to mark an item as completed in that journal entry with a TUI checkbox
|
|
|
|
todo_group = parser.add_mutually_exclusive_group()
|
|
|
|
todo_group.add_argument( "-d", "--todo", help="Manage pending tasks.", action="store_true" )
|
|
|
|
|
|
|
|
# should pull up a curses menu to select from all topics for all dates. selecting a topic pulls up a list
|
|
|
|
# of all dates for that topic.
|
|
|
|
list_topic_group = parser.add_mutually_exclusive_group()
|
|
|
|
list_topic_group.add_argument( "-L", "--list-topics", help="Browse topics for all dates.", action="store_true" )
|
|
|
|
|
|
|
|
# should pull up a curses menu list of all dates. selecting a date should list topics for that date.
|
|
|
|
list_dates_group = parser.add_mutually_exclusive_group()
|
|
|
|
list_dates_group.add_argument( "-l", "--list-dates", help="Browse dates for all topics.", action="store_true" )
|
|
|
|
|
|
|
|
# opens a new topic with today's date using the selected editor specified in the config file
|
|
|
|
new_entry_group = parser.add_mutually_exclusive_group()
|
|
|
|
new_entry_group.add_argument( "-o", "--new-topic")
|
|
|
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
config = Config()
|
|
|
|
silojourn = Journaler( config )
|
|
|
|
|
|
|
|
if len(sys.argv) > 3:
|
|
|
|
parser.print_help()
|
|
|
|
|
|
|
|
elif args.date:
|
|
|
|
silojourn.do_topic_browse_by_date( args.date )
|
|
|
|
|
|
|
|
elif args.topic:
|
|
|
|
silojourn.do_date_browse_by_topic( args.topic )
|
|
|
|
|
|
|
|
elif args.todo:
|
|
|
|
silojourn.do_browse_todo_entries()
|
|
|
|
|
|
|
|
elif args.list_topics:
|
|
|
|
silojourn.do_browse_topics()
|
|
|
|
|
|
|
|
elif args.list_dates:
|
|
|
|
silojourn.do_list_dates()
|
|
|
|
|
|
|
|
elif args.new_topic:
|
|
|
|
silojourn.do_new_topic( args.new_topic )
|
|
|
|
|
|
|
|
else:
|
|
|
|
parser.print_help()
|
|
|
|
|
|
|
|
|
|
|
|
if __name__=='__main__':
|
|
|
|
main()
|