mirror of
https://github.com/chemistry-software/Periodic-Table-JSON.git
synced 2026-11-07 09:17:31 +00:00
Refactor specific_data.py (#64)
* Fix unit of molar heat * Adjust command line parser and help messages The adjustmenst reflect how the code as is actually works and the extended help message give actual examples of how to use the command. * Clarify help message * Make command executable * Add option to quit adding properties * Refactor output to use functionality from argparse Default behavior is now more robust for output. Changed write functions to use less of global state. * Parameterize help message fors default output file name * Improve prompt to quit interactive loop * Add example with properties and interactive * Remove debug cruft * Refactor write functions to not use global variables * Skip already selected in interactive phase given by properites * Improve help message * Refactor logic for output in the default case USe the functionality provided by argparse for default values of an optional command line argument. In this case --output. * Restructure code by moving already defined functions up * Refactor structure of code to use functions This is s step towards further simplification of each step. * Refactor use of keys and clear screen for interactive use only * Add the main guard * Use argparse hook to parse comma separated properties This provides a list of arguments directly. Find the set intersection and difference to filter out correctly named properties. Structure the code so as to keep the terminal output separate. * Refactor interactive paring to us local functions * Use result provided by default_input * Use pylint to clean up * Improve docstrings * Format help message * Refactor name and replace with namedtuple
This commit is contained in:
1 parent
640bd393b4
commit
ea41119626
1 file changed
+281
-92
Regular → Executable
+281
-92
@@ -1,131 +1,320 @@
|
||||
import json, os, argparse
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""Select specific data about elements and write them to a named file.
|
||||
|
||||
Use the --help for options and examples.
|
||||
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
import textwrap
|
||||
import json
|
||||
from pathlib import Path
|
||||
from collections import namedtuple
|
||||
|
||||
ColorCodes = namedtuple('ColorCodes', ['PURPLE', 'CYAN', 'DARKCYAN',
|
||||
'BLUE', 'GREEN', 'YELLOW',
|
||||
'RED', 'BOLD', 'UNDERLINE',
|
||||
'END',])
|
||||
|
||||
color_code = ColorCodes(PURPLE='\033[95m', CYAN='\033[96m', DARKCYAN='\033[36m',
|
||||
BLUE='\033[94m', GREEN='\033[92m', YELLOW='\033[93m',
|
||||
RED='\033[91m', BOLD='\033[1m', UNDERLINE='\033[4m',
|
||||
END='\033[0m',)
|
||||
|
||||
# pylint: disable=missing-function-docstring
|
||||
def create_commandeline_parser(default_file):
|
||||
parser = argparse.ArgumentParser(
|
||||
description=__doc__,
|
||||
epilog=textwrap.dedent(f"""
|
||||
examples:
|
||||
Properties written to a json file:
|
||||
$ {sys.argv[0]} --properties=name,atomic_mass --output name_mass.json
|
||||
|
||||
Properties written to a csv file:
|
||||
$ {sys.argv[0]} --properties name,atomic_mass --output name_mass.csv
|
||||
|
||||
Properties written into both files {default_file}.json and {default_file}.csv:
|
||||
$ {sys.argv[0]} --properties=name,atomic_mass
|
||||
|
||||
Union of properties written into both files {default_file}.json and {default_file}.csv:
|
||||
$ {sys.argv[0]} --properties=name,atomic_mass --interactive
|
||||
|
||||
Select properties interactively and write to files {default_file}.json and {default_file}.csv:
|
||||
$ {sys.argv[0]} --interactive
|
||||
|
||||
NOTE: output files are written to the directory above {sys.argv[0]}.
|
||||
|
||||
"""),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
|
||||
def comma_separated(arg):
|
||||
return arg.split(",")
|
||||
parser.add_argument('--properties', type=comma_separated, metavar='P1,...', nargs=1,
|
||||
help='comma separated list of properties')
|
||||
|
||||
parser.add_argument('--interactive', action="store_true",
|
||||
help='whether to interactively select data')
|
||||
|
||||
parser.add_argument('--output', metavar='FILENAME', nargs='?', const=default_file, default='',
|
||||
help=f'where to output the data (default: {default_file}.{{json,csv}})')
|
||||
return parser
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(description='Selects specific data about elements and outputs it.')
|
||||
parser.add_argument('--properties', metavar='-p', type=list, nargs='+',
|
||||
help='properties to select')
|
||||
def read_periodic_table():
|
||||
"""
|
||||
Load and return elements from the Periodic Table JSON file.
|
||||
|
||||
parser.add_argument('--output', metavar='-o', type=str, nargs='+',
|
||||
help='where to output the data')
|
||||
This function reads the Periodic Table data from a JSON file located
|
||||
one directory above the current file's directory.
|
||||
|
||||
parser.add_argument('--interactive', metavar='-i', type=bool, nargs='+', default=False,
|
||||
help='whether to interactively select data')
|
||||
Returns:
|
||||
tuple: A tuple containing two elements:
|
||||
- A list of dictionaries, where each dictionary represents an element
|
||||
with its properties as key-value pairs.
|
||||
- A set of strings, representing the keys (properties) available in
|
||||
the element dictionaries.
|
||||
"""
|
||||
with open(os.path.join(Path(__file__).parents[1],'PeriodicTableJSON.json'),
|
||||
encoding="utf8") as file:
|
||||
elements = json.load(file)['elements']
|
||||
return elements, elements[0].keys()
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
def clear():
|
||||
os.system('cls' if os.name == 'nt' else 'clear')
|
||||
def parse_properties(data_needed, args, keys):
|
||||
"""
|
||||
Update data_needed with properties specified in args if they exist in keys.
|
||||
|
||||
class c:
|
||||
PURPLE = '\033[95m'
|
||||
CYAN = '\033[96m'
|
||||
DARKCYAN = '\033[36m'
|
||||
BLUE = '\033[94m'
|
||||
GREEN = '\033[92m'
|
||||
YELLOW = '\033[93m'
|
||||
RED = '\033[91m'
|
||||
BOLD = '\033[1m'
|
||||
UNDERLINE = '\033[4m'
|
||||
END = '\033[0m'
|
||||
Parameters:
|
||||
data_needed (dict): A dictionary where each key represents a property name,
|
||||
and the value indicates whether the property is needed (True or False).
|
||||
args (argparse.Namespace): An object containing command line arguments.
|
||||
This function expects args to have a 'properties' attribute, which is
|
||||
a list of lists containing property names as strings.
|
||||
keys (list): A list of strings representing valid property names.
|
||||
|
||||
full_path = os.getcwd().split('\\')
|
||||
full_path.pop()
|
||||
Returns:
|
||||
dict: The updated data_needed dictionary with keys set to True for properties
|
||||
that are both requested in args and exist in keys.
|
||||
|
||||
clear()
|
||||
with open(os.path.join(Path(__file__).parents[1], 'PeriodicTableJSON.json'), encoding="utf8") as f:
|
||||
elements = json.load(f)['elements']
|
||||
Side Effects:
|
||||
- Prints messages to the console indicating whether each requested property
|
||||
was found or not found.
|
||||
- Modifies the data_needed dictionary in-place by updating the values for
|
||||
keys that represent valid properties.
|
||||
"""
|
||||
def show_bad(pval):
|
||||
message = ''.join([color_code.RED, 'Property ',
|
||||
color_code.BLUE, pval,
|
||||
color_code.RED, ' not found.',
|
||||
color_code.END,])
|
||||
print(message)
|
||||
|
||||
data_needed = {}
|
||||
first_element = elements[0]
|
||||
def show_good(pval):
|
||||
message = ''.join([color_code.GREEN, 'Property ',
|
||||
color_code.BLUE, pval,
|
||||
color_code.GREEN,' found.',
|
||||
color_code.END,])
|
||||
print(message)
|
||||
|
||||
keys = first_element.keys()
|
||||
if args.properties:
|
||||
properties = args.properties[0]
|
||||
props = []
|
||||
z = ""
|
||||
for x in properties:
|
||||
if x == ',':
|
||||
props.append(z)
|
||||
z = ""
|
||||
else:
|
||||
z += x
|
||||
if z != "":
|
||||
props.append(z)
|
||||
props = set(args.properties[0])
|
||||
keys_as_set = set(keys)
|
||||
bad = props - keys_as_set
|
||||
good = props & keys_as_set
|
||||
data_needed.update({k:True for k in good})
|
||||
|
||||
for prop in props:
|
||||
if prop not in keys:
|
||||
print(c.RED + 'Property ' + prop + ' not found.' + c.END)
|
||||
props.remove(prop)
|
||||
else:
|
||||
data_needed[prop] = True
|
||||
print(c.GREEN + 'Property ' + prop + ' found.' + c.END)
|
||||
for pval in bad:
|
||||
show_bad(pval)
|
||||
for pval in good:
|
||||
show_good(pval)
|
||||
return data_needed
|
||||
|
||||
for p in props:
|
||||
data_needed[p] = True
|
||||
|
||||
if args.interactive:
|
||||
def parse_interactive(data_needed, keys):
|
||||
"""
|
||||
Interactively update the data_needed dictionary based on user input.
|
||||
|
||||
This function iterates over a list of keys, presenting each to the user
|
||||
and asking if it is needed. The user's response is used to update the
|
||||
data_needed dictionary: setting the value to True for keys that are needed.
|
||||
|
||||
Parameters:
|
||||
data_needed (dict): A dictionary where each key represents a potential
|
||||
option, and the value (True/False) indicates whether it is needed.
|
||||
keys (list): A list of strings representing all possible options that
|
||||
the user can choose from.
|
||||
|
||||
Returns:
|
||||
dict: The updated data_needed dictionary reflecting the user's choices.
|
||||
Keys corresponding to needed options are set to True.
|
||||
|
||||
|
||||
Note:
|
||||
The function calls an external 'clear()' function to clear the console
|
||||
before each iteration, improving readability of the interactive session.
|
||||
"""
|
||||
def show_selected():
|
||||
message = ''.join([
|
||||
color_code.BOLD, color_code.GREEN,
|
||||
f'{len(data_needed)}', ' Option(s) Selected ',
|
||||
color_code.END,
|
||||
color_code.UNDERLINE, f'{list(data_needed.keys())}',
|
||||
color_code.END,
|
||||
])
|
||||
print(message)
|
||||
|
||||
def show_next():
|
||||
message = ''.join([
|
||||
'Do you need ',
|
||||
color_code.BOLD, color_code.BLUE, key, '? ',
|
||||
color_code.END,
|
||||
])
|
||||
print(message)
|
||||
|
||||
def default_input(prompt, default='y'):
|
||||
user_input = input(prompt)
|
||||
return user_input if user_input else default
|
||||
|
||||
def select_next():
|
||||
prompt = ''.join([
|
||||
color_code.GREEN, 'y/',
|
||||
color_code.RED, 'n/',
|
||||
color_code.BLUE, 'q(uit)',
|
||||
color_code.END, '[',
|
||||
color_code.PURPLE, 'default: ',
|
||||
color_code.GREEN, 'y',
|
||||
color_code.END, ']:',
|
||||
])
|
||||
return default_input(prompt)
|
||||
|
||||
def clear():
|
||||
os.system('cls' if os.name == 'nt' else 'clear')
|
||||
|
||||
clear()
|
||||
for key in keys:
|
||||
if key in data_needed:
|
||||
continue
|
||||
done = False
|
||||
while True:
|
||||
print(f'{c.BOLD + c.GREEN}{len(data_needed.keys())} Option(s) Selected: {c.END}{c.UNDERLINE}{list(data_needed.keys())}')
|
||||
print(f'{c.END}Do you need {c.BOLD + c.CYAN}{key}? {c.END}')
|
||||
needed = input(f'({c.GREEN}y/{c.RED}n{c.END}) [{c.PURPLE}default: {c.GREEN}y{c.END}]: ')
|
||||
show_selected()
|
||||
show_next()
|
||||
needed = select_next()
|
||||
if needed == 'y':
|
||||
data_needed[key] = True
|
||||
elif needed == 'n':
|
||||
break
|
||||
else:
|
||||
print('Invalid input')
|
||||
continue
|
||||
if needed == 'n':
|
||||
break
|
||||
if needed == 'q':
|
||||
done = True
|
||||
break
|
||||
print('Invalid input')
|
||||
continue
|
||||
if done:
|
||||
break
|
||||
clear()
|
||||
return data_needed
|
||||
|
||||
|
||||
if len(data_needed.keys()) == 0:
|
||||
print(c.RED + 'No properties selected.' + c.END)
|
||||
exit()
|
||||
def save2file(args, elements, data_needed, default_file):
|
||||
"""
|
||||
Save elements to file(s) in JSON and/or CSV format based on args.
|
||||
|
||||
def writeCSV():
|
||||
with open(os.path.join(Path(__file__).parents[1], output + '.csv'), 'w', encoding="utf8") as f:
|
||||
This function writes the specified elements to a file or files, using
|
||||
JSON and/or CSV formats.
|
||||
|
||||
Parameters:
|
||||
args: An object containing command line arguments, including 'output'
|
||||
which specifies the output file name and format.
|
||||
elements (list): A list of dictionaries, each representing an element
|
||||
with data to be saved.
|
||||
data_needed (dict): A dictionary specifying which data from elements
|
||||
should be included in the output file(s).
|
||||
default_file (str): The default file name to use if no output file is
|
||||
specified or if the specified file is the default.
|
||||
"""
|
||||
if not args.output or args.output == default_file:
|
||||
# Use default and write to both csv and json files.
|
||||
write_json(default_file, elements, data_needed)
|
||||
write_csv(default_file, elements, data_needed)
|
||||
else:
|
||||
# Write to provided file name.
|
||||
if ('json' in args.output.lower()) or ('csv' in args.output.lower()):
|
||||
output = args.output.replace('.json', '').replace('.csv', '')
|
||||
if 'json' in args.output.lower():
|
||||
write_json(output, elements, data_needed)
|
||||
if 'csv' in args.output.lower():
|
||||
write_csv(output, elements, data_needed)
|
||||
|
||||
|
||||
def write_csv(output, elements, data_needed):
|
||||
"""
|
||||
Write selected data from elements to a CSV file.
|
||||
|
||||
Parameters:
|
||||
output (str): The base name of the output file.
|
||||
elements (list): A list of dictionaries with element data.
|
||||
data_needed (dict): A dict indicating which keys to include.
|
||||
"""
|
||||
with open(os.path.join(Path(__file__).parents[1], output + '.csv'),
|
||||
'w', encoding="utf8") as file:
|
||||
elem_to_write = []
|
||||
elem_to_write.append(','.join(data_needed.keys()))
|
||||
for element in elements:
|
||||
e = ""
|
||||
elmnt = ""
|
||||
for key in data_needed:
|
||||
if data_needed[key]:
|
||||
e += str(element[key]) + ','
|
||||
if(e[-1] == ','):
|
||||
e = e[:-1]
|
||||
elem_to_write.append(e)
|
||||
elmnt += str(element[key]) + ','
|
||||
if elmnt[-1] == ',':
|
||||
elmnt = elmnt[:-1]
|
||||
elem_to_write.append(elmnt)
|
||||
file.write("\n".join(elem_to_write))
|
||||
file.write('\n')
|
||||
|
||||
f.write("\n".join(elem_to_write))
|
||||
f.write('\n')
|
||||
def writeJSON():
|
||||
with open(os.path.join(Path(__file__).parents[1], output + '.json'), 'w') as f:
|
||||
|
||||
def write_json(output, elements, data_needed):
|
||||
"""
|
||||
Write selected data from elements to a JSON file.
|
||||
|
||||
Parameters:
|
||||
output (str): The base name of the output file.
|
||||
elements (list): A list of dictionaries with element data.
|
||||
data_needed (dict): A dict indicating which keys to write.
|
||||
"""
|
||||
with open(os.path.join(Path(__file__).parents[1], output + '.json'),
|
||||
'w', encoding="utf8") as file:
|
||||
elem_to_write = []
|
||||
|
||||
for element in elements:
|
||||
e = {}
|
||||
elmnt = {}
|
||||
for key in data_needed:
|
||||
if data_needed[key]:
|
||||
e[key] = element[key]
|
||||
elem_to_write.append(e)
|
||||
elmnt[key] = element[key]
|
||||
elem_to_write.append(elmnt)
|
||||
file.write(json.dumps(elem_to_write, indent=4))
|
||||
file.write('\n')
|
||||
|
||||
f.write(json.dumps(elem_to_write, indent=4))
|
||||
f.write('\n')
|
||||
|
||||
output = 'SpecificData'
|
||||
printz = ''
|
||||
if args.output:
|
||||
if ('json' in args.output[0].lower()) or ('csv' in args.output[0].lower()):
|
||||
output = args.output[0].replace('.json', '').replace('.csv', '')
|
||||
# pylint: disable=missing-function-docstring
|
||||
def main():
|
||||
default_file = 'SpecificData'
|
||||
parser = create_commandeline_parser(default_file)
|
||||
|
||||
if 'json' in args.output[0].lower():
|
||||
writeJSON()
|
||||
exit()
|
||||
if 'csv' in args.output[0].lower():
|
||||
writeCSV()
|
||||
exit()
|
||||
else:
|
||||
writeJSON()
|
||||
writeCSV()
|
||||
args = parser.parse_args()
|
||||
elements, keys = read_periodic_table()
|
||||
|
||||
data_needed = {}
|
||||
if args.properties:
|
||||
data_needed = parse_properties(data_needed, args, keys)
|
||||
if args.interactive:
|
||||
data_needed = parse_interactive(data_needed, keys)
|
||||
if data_needed:
|
||||
save2file(args, elements, data_needed, default_file)
|
||||
else:
|
||||
print(color_code.RED + 'No properties selected.' + color_code.END)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in new issue
Block a user