Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

python module #58

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
__pycache__
.idea
venv
periodic_table.egg-info
table.py
37 changes: 37 additions & 0 deletions periodic_table/README.md
Original file line number Diff line number Diff line change
@@ -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*.

36 changes: 36 additions & 0 deletions periodic_table/generate/element_template.py
Original file line number Diff line number Diff line change
@@ -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
"""
142 changes: 142 additions & 0 deletions periodic_table/generate/generate_module.py
Original file line number Diff line number Diff line change
@@ -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()
37 changes: 37 additions & 0 deletions periodic_table/generate/readme_template.py
Original file line number Diff line number Diff line change
@@ -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*.
"""
32 changes: 32 additions & 0 deletions periodic_table/generate/table_template.py
Original file line number Diff line number Diff line change
@@ -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
"""
7 changes: 7 additions & 0 deletions periodic_table/generate/testdef.py
Original file line number Diff line number Diff line change
@@ -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)
4 changes: 4 additions & 0 deletions periodic_table/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

41 changes: 41 additions & 0 deletions periodic_table/setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
[metadata]
name = periodic_table_dataclasses
version = attr: periodic_table.__version__
author = Gerard
author_email = [email protected]
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

5 changes: 5 additions & 0 deletions periodic_table/src/periodic_table/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .table import Element, PeriodicTable


__version__ = 1.0

35 changes: 35 additions & 0 deletions periodic_table/src/periodic_table/main.py
Original file line number Diff line number Diff line change
@@ -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()

Loading