diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..413384a --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +__pycache__ +.idea +venv +periodic_table.egg-info +table.py diff --git a/periodic_table/README.md b/periodic_table/README.md new file mode 100644 index 0000000..dc8ab2c --- /dev/null +++ b/periodic_table/README.md @@ -0,0 +1,37 @@ +A Python library of periodic data statically generated from **PeriodicTableJSON.json** (https://github.com/NMRbox/Periodic-Table-JSON) on 2023 Jan 28. + +An *Element* dataclass and *PeriodicTable* container class is generated from the JSON data. + +Currently only the single valued str, float, and int are supported. The JSON fields *shells*, *ionization_energies*, *image* are omitted. + + +# Installation + pip install periodic_table_dataclasses + +# Usage + + from periodic_table import PeriodicTable + pt = PeriodicTable() + h = pt.search_name('hydrogen') + s = pt.search_number(16) + fe = pt.search_symbol('Fe') + for element in pt.elements: + print(element) + +# Discussion +### Unnecessary +This module is not necessary to use PeriodicTableJSON.json in Python. + + with open('PeriodicTableJSON.json') as f: + data = json.load(f) + elements = data['elements'] + +will bring all data into Pyton as nested data structure. + +### Convenient +The module was implemented for the convenience of named class fields. A static definition allows type +checking and code completion when working in Python integrated development environments (IDE). + +### Additional feature +The *PeriodicTable.search_name* features supports the British spellings *aluminium* and *sulphur*. + diff --git a/periodic_table/generate/element_template.py b/periodic_table/generate/element_template.py new file mode 100755 index 0000000..e6f458d --- /dev/null +++ b/periodic_table/generate/element_template.py @@ -0,0 +1,36 @@ +# +# template for Element dataclass +# +_ELEMENT_START= """import io +from dataclasses import dataclass, field,fields +from dataclasses_json import dataclass_json +from typing import Iterable, List, Optional + + +@dataclass_json +@dataclass +class Element: + name: str + atomic: int + symbol: str""" + +_ELEMENT_TAIL =""" _altnames : List[str] = field(default_factory=list) + + def __str__(self): + buffer = io.StringIO() + print(f'Element {self.name}',file=buffer) + names = [f.name for f in fields(self) if not f.name.startswith('_') and f.name != 'name'] + nlen = max([len(n) for n in names]) + for name in names: + print(f' {name:{nlen}} = {getattr(self,name)}',file=buffer) + return buffer.getvalue() + + def setnames(self,names:Iterable[str])->None: + self._altnames = [n.lower() for n in names] + + + def is_named(self,value)->bool: + \"""Case-insensitive search of names\""" + svalue = value.lower() + return svalue == self.name.lower() or svalue in self._altnames +""" diff --git a/periodic_table/generate/generate_module.py b/periodic_table/generate/generate_module.py new file mode 100755 index 0000000..ea7ed0a --- /dev/null +++ b/periodic_table/generate/generate_module.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +import argparse +import collections +import datetime +import json +import logging +import os +from typing import List + +from element_template import _ELEMENT_START, _ELEMENT_TAIL +from table_template import _PERIODIC_START, _PERIODIC_TAIL +from readme_template import _README + +_logger = logging.getLogger(__name__) +_ALIASES = ( + # British spellings + (13, ('aluminium',)), + (16, ('sulphur',)), +) + +JSON_SOURCE = 'PeriodicTableJSON.json' + + +class CodeBuilder: + + def __init__(self): + self.our_directory = os.path.dirname(__file__) + src = os.path.join(self.our_directory, '..', '..', JSON_SOURCE) + self.jsource = os.path.abspath(src) + if not os.path.isfile(self.jsource): + raise ValueError(f"{self.jsource} not found") + self.skipped : List[str] = [] + + def __enter__(self): + script = os.path.join(os.path.dirname(__file__), '..', 'src', 'periodic_table', 'table.py') + self.code = open(script, 'w') + self.datestamp = datetime.datetime.now().strftime('%Y %b %d') + print(f'# generated from {JSON_SOURCE} {self.datestamp}. Do not hand edit',file=self.code) + print(_ELEMENT_START, file=self.code) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.code.close() + pass + + + def generate(self): + self._read_json() + self._build_element() + self._build_table() + self._generate_readme() + + def _read_json(self): + attrs = collections.defaultdict(int) + with open(self.jsource) as f: + data = json.load(f) + self.raw_data = data['elements'] + for e in self.raw_data: + for d in e.keys(): + attrs[d] += 1 + expected = list(attrs.values())[0] + bad = False + for a, num in attrs.items(): + if num != expected: + print(f"{a} has {num} implementors, not {expected}") + bad = True + if bad: + raise ValueError("length mismatch") + return attrs + + def _cleanup(self, identifier): + """Cleanup JSON value into valid Python identifier""" + return identifier.replace('-', '_') + + def _build_element(self): + _SUPPORTED = (str, float, int) + # start with these + self.field_order: List[str] = ['name', 'number', 'symbol'] + example = self.raw_data[0] + for attr in self.field_order: + if attr not in example: + raise ValueError(f'{attr} missing') + adding = [] + for k, v in example.items(): + _logger.debug(f"{k} {type(v)}") + if k in self.field_order: + continue + if type(v) not in _SUPPORTED: + _logger.info(f"Skipping {k} {type(v)}") + self.skipped.append(k) + continue + adding.append(k) + adding = sorted(adding) + self.field_order.extend(adding) + _logger.debug(self.field_order) + for field in adding: + v = example[field] + tsting = type(v).__name__ + print(f' {self._cleanup(field)} : Optional[{tsting}]', file=self.code) + print(_ELEMENT_TAIL, file=self.code) + + def _build_table(self): + print(_PERIODIC_START, file=self.code) + for e in self.raw_data: + print(12 * ' ' + 'Element(', end='', file=self.code) + values = [] + for fld in self.field_order: + value = e[fld] + if isinstance(value, str): + values.append(f'"""{value}"""') + else: + values.append(str(value)) + print(f"{','.join(values)}),", file=self.code) + print(8 * ' ' + ')', file=self.code) + for atomic, names in _ALIASES: + namestrs = [f"'{n}'" for n in names] + setter = f" self.search_number({atomic}).setnames([{','.join(namestrs)}])" + print(setter, file=self.code) + + print(_PERIODIC_TAIL, file=self.code) + + def _generate_readme(self): + missing = ', '.join([f'*{m}*' for m in self.skipped]) + with open(os.path.join(self.our_directory,'..','README.md'),'w') as f: + print(_README.format(datestamp=self.datestamp,missing=missing),file=f) + + + + +def main(): + logging.basicConfig() + parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument('-l', '--loglevel', default='WARN', help="Python logging level") + + args = parser.parse_args() + _logger.setLevel(getattr(logging, args.loglevel)) + with CodeBuilder() as builder: + builder.generate() + + +if __name__ == "__main__": + main() diff --git a/periodic_table/generate/readme_template.py b/periodic_table/generate/readme_template.py new file mode 100644 index 0000000..8b42ec4 --- /dev/null +++ b/periodic_table/generate/readme_template.py @@ -0,0 +1,37 @@ +_README="""A Python library of periodic data statically generated from **PeriodicTableJSON.json** (https://github.com/NMRbox/Periodic-Table-JSON) on {datestamp}. + +An *Element* dataclass and *PeriodicTable* container class is generated from the JSON data. + +Currently only the single valued str, float, and int are supported. The JSON fields {missing} are omitted. + + +# Installation + pip install periodic_table_dataclasses + +# Usage + + from periodic_table import PeriodicTable + pt = PeriodicTable() + h = pt.search_name('hydrogen') + s = pt.search_number(16) + fe = pt.search_symbol('Fe') + for element in pt.elements: + print(element) + +# Discussion +### Unnecessary +This module is not necessary to use PeriodicTableJSON.json in Python. + + with open('PeriodicTableJSON.json') as f: + data = json.load(f) + elements = data['elements'] + +will bring all data into Pyton as nested data structure. + +### Convenient +The module was implemented for the convenience of named class fields. A static definition allows type +checking and code completion when working in Python integrated development environments (IDE). + +### Additional feature +The *PeriodicTable.search_name* features supports the British spellings *aluminium* and *sulphur*. +""" diff --git a/periodic_table/generate/table_template.py b/periodic_table/generate/table_template.py new file mode 100755 index 0000000..48e90e8 --- /dev/null +++ b/periodic_table/generate/table_template.py @@ -0,0 +1,32 @@ +# +# Template for PeriodicTable class +# +_PERIODIC_START = """ + +class PeriodicTable: + + def __init__(self): + self.elements = (""" + +_PERIODIC_TAIL = """ def search_name(self,name:str)-> Optional[Element]: + \"""Case-insensitive British / American search for element name\""" + for e in self.elements: + if e.is_named(name): + return e + return None + + def search_symbol(self,symbol:str)-> Optional[Element]: + \"""Case-insensitive search for element symbol\""" + lsymbol = symbol.lower() + for e in self.elements: + if lsymbol == e.symbol.lower(): + return e + return None + + def search_number(self,number:int)-> Optional[Element]: + \"""Search by atomic number\""" + for e in self.elements: + if e.atomic == number: + return e + return None +""" diff --git a/periodic_table/generate/testdef.py b/periodic_table/generate/testdef.py new file mode 100644 index 0000000..c615edc --- /dev/null +++ b/periodic_table/generate/testdef.py @@ -0,0 +1,7 @@ +from generate.updated_definition import PeriodicTable + +pt = PeriodicTable() +for e in pt.elements: + print(f"{e.name} {e.atomic}") +s = pt.search_number(16) +print(s) diff --git a/periodic_table/pyproject.toml b/periodic_table/pyproject.toml new file mode 100644 index 0000000..890c3fc --- /dev/null +++ b/periodic_table/pyproject.toml @@ -0,0 +1,4 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + diff --git a/periodic_table/setup.cfg b/periodic_table/setup.cfg new file mode 100644 index 0000000..3d0f616 --- /dev/null +++ b/periodic_table/setup.cfg @@ -0,0 +1,41 @@ +[metadata] +name = periodic_table_dataclasses +version = attr: periodic_table.__version__ +author = Gerard +author_email = gweatherby@uchc.edu +description = Python library of Periodic-Table-JSON +long_description = file: README.md +long_description_content_type = text/markdown +license = CC BY-SA 3.0 +#url = https://github.com/Bowserinator/Periodic-Table-JSON +url = https://github.com/NMRbox/Periodic-Table-JSON + + +classifier: + License :: Other/Proprietary License + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 + +[options] +#include_package_data = True +package_dir = + = src +packages = + periodic_table +install_requires = + dataclasses_json + +[options.entry_points] +console_scripts = + periodic_table = periodic_table.main:main + +[build_ext] +debug = 1 + +[options.package_data] +* = README.md + diff --git a/periodic_table/src/periodic_table/__init__.py b/periodic_table/src/periodic_table/__init__.py new file mode 100644 index 0000000..64f1eac --- /dev/null +++ b/periodic_table/src/periodic_table/__init__.py @@ -0,0 +1,5 @@ +from .table import Element, PeriodicTable + + +__version__ = 1.0 + diff --git a/periodic_table/src/periodic_table/main.py b/periodic_table/src/periodic_table/main.py new file mode 100644 index 0000000..e846dd6 --- /dev/null +++ b/periodic_table/src/periodic_table/main.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +import argparse +import dataclasses +import logging +from pprint import pprint + +from periodic_table import PeriodicTable + + +def main(): + logging.basicConfig() + parser = argparse.ArgumentParser() + group = parser.add_mutually_exclusive_group() + + group.add_argument('--name',help="search by element name") + group.add_argument('--number',type=int, help="search by atomic number") + group.add_argument('--symbol', help="search by symbol") + + args = parser.parse_args() + pt = PeriodicTable() + e = None + if args.name: + e = pt.search_name(args.name) + if args.number: + e = pt.search_number(args.number) + if args.symbol: + e = pt.search_symbol(args.number) + if e: + pprint(dataclasses.asdict(e)) + + + +if __name__ == "__main__": + main() + diff --git a/periodic_table/src/tests/test_module.py b/periodic_table/src/tests/test_module.py new file mode 100644 index 0000000..6d28f75 --- /dev/null +++ b/periodic_table/src/tests/test_module.py @@ -0,0 +1,39 @@ +from unittest import TestCase + +from periodic_table import PeriodicTable + + +class TestPeriodicTable(TestCase): + + def setUp(self) -> None: + self.table = PeriodicTable() + +# def test_print(self): +# print(self.table.search_number(1)) + + def test_search_name(self): + s = self.table.search_number(16) + self.assertFalse(s is None) + names = ('sulfur', 'Sulfur', 'Sulphur') + for n in names: + self.assertTrue(s.is_named(n)) + found = self.table.search_name(n) + self.assertTrue(found is s) + + self.assertIsNone(self.table.search_name("Gerardominium")) + + def test_search_symbol(self): + fe = self.table.search_number(26) + for sm in ('Fe','fe'): + found = self.table.search_symbol(sm) + self.assertTrue(found is fe) + self.assertIsNone(self.table.search_symbol('peace sign')) + + + def test_search_number(self): + for i in range(1,115): + e = self.table.search_number(i) + self.assertEqual(i,e.atomic) + + +