chemql-python 0.2.2__py3-none-any.whl → 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chemql/__init__.py +425 -21
- chemql_python-1.0.1.dist-info/METADATA +443 -0
- {chemql_python-0.2.2.dist-info → chemql_python-1.0.1.dist-info}/RECORD +7 -7
- chemql_python-0.2.2.dist-info/METADATA +0 -309
- {chemql_python-0.2.2.dist-info → chemql_python-1.0.1.dist-info}/WHEEL +0 -0
- {chemql_python-0.2.2.dist-info → chemql_python-1.0.1.dist-info}/entry_points.txt +0 -0
- {chemql_python-0.2.2.dist-info → chemql_python-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {chemql_python-0.2.2.dist-info → chemql_python-1.0.1.dist-info}/top_level.txt +0 -0
chemql/__init__.py
CHANGED
|
@@ -5,9 +5,13 @@ import re
|
|
|
5
5
|
import ast
|
|
6
6
|
import argparse
|
|
7
7
|
import time
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
8
10
|
from pathlib import Path
|
|
9
11
|
from prompt_toolkit import PromptSession
|
|
10
12
|
from prompt_toolkit.lexers import Lexer
|
|
13
|
+
import atexit
|
|
14
|
+
import uuid
|
|
11
15
|
from prompt_toolkit.styles import Style
|
|
12
16
|
from prompt_toolkit.completion import Completer, Completion
|
|
13
17
|
import urllib.parse
|
|
@@ -28,9 +32,91 @@ __all__ = [
|
|
|
28
32
|
"balance_stoichiometry",
|
|
29
33
|
"execute_query",
|
|
30
34
|
"execute_query_text",
|
|
35
|
+
"ChemQLQuery",
|
|
36
|
+
"search",
|
|
37
|
+
"union",
|
|
38
|
+
"intersection",
|
|
31
39
|
]
|
|
32
40
|
|
|
33
41
|
|
|
42
|
+
|
|
43
|
+
_shell_process = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_shell():
|
|
47
|
+
"""Start Bash once and reuse the same session."""
|
|
48
|
+
global _shell_process
|
|
49
|
+
|
|
50
|
+
if _shell_process is None or _shell_process.poll() is not None:
|
|
51
|
+
_shell_process = subprocess.Popen(
|
|
52
|
+
["bash", "--noprofile", "--norc"],
|
|
53
|
+
stdin=subprocess.PIPE,
|
|
54
|
+
stdout=subprocess.PIPE,
|
|
55
|
+
stderr=subprocess.STDOUT,
|
|
56
|
+
text=True,
|
|
57
|
+
bufsize=1,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return _shell_process
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def run_shell_command(command):
|
|
64
|
+
"""Run a command in the persistent Bash session."""
|
|
65
|
+
shell = get_shell()
|
|
66
|
+
marker = f"__CHEMQL_END_{uuid.uuid4().hex}__"
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
shell.stdin.write(command + "\n") #type:ignore
|
|
70
|
+
shell.stdin.write( #type:ignore
|
|
71
|
+
f"printf '\\n%s:%s\\n' '{marker}' \"$?\"\n"
|
|
72
|
+
)
|
|
73
|
+
shell.stdin.flush() #type:ignore
|
|
74
|
+
|
|
75
|
+
output = []
|
|
76
|
+
exit_code = None
|
|
77
|
+
|
|
78
|
+
while True:
|
|
79
|
+
line = shell.stdout.readline() #type:ignore
|
|
80
|
+
|
|
81
|
+
if not line:
|
|
82
|
+
break
|
|
83
|
+
|
|
84
|
+
line = line.rstrip("\n")
|
|
85
|
+
|
|
86
|
+
if line.startswith(marker + ":"):
|
|
87
|
+
exit_code = int(line[len(marker) + 1:])
|
|
88
|
+
break
|
|
89
|
+
|
|
90
|
+
output.append(line)
|
|
91
|
+
|
|
92
|
+
result = "\n".join(output)
|
|
93
|
+
|
|
94
|
+
if exit_code is None:
|
|
95
|
+
return result or "Shell session terminated."
|
|
96
|
+
|
|
97
|
+
if exit_code != 0 and not result:
|
|
98
|
+
return f"Command exited with status {exit_code}"
|
|
99
|
+
|
|
100
|
+
return result
|
|
101
|
+
|
|
102
|
+
except (BrokenPipeError, OSError) as exc:
|
|
103
|
+
return f"Shell error: {exc}"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def close_shell():
|
|
107
|
+
"""Clean up the persistent shell on exit."""
|
|
108
|
+
global _shell_process
|
|
109
|
+
|
|
110
|
+
if _shell_process is not None:
|
|
111
|
+
if _shell_process.poll() is None:
|
|
112
|
+
_shell_process.terminate()
|
|
113
|
+
_shell_process.wait(timeout=2)
|
|
114
|
+
|
|
115
|
+
_shell_process = None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
atexit.register(close_shell)
|
|
119
|
+
|
|
34
120
|
class Unknown:
|
|
35
121
|
"""Represents a Chemql query result that cannot be resolved to a known object."""
|
|
36
122
|
|
|
@@ -112,7 +198,7 @@ with (DATA_DIR / "MoleculeBonds.json").open("r", encoding="utf-8") as f:
|
|
|
112
198
|
with (DATA_DIR / "BondEnergies.json").open("r", encoding="utf-8") as f:
|
|
113
199
|
bondEnergies = json.load(f)
|
|
114
200
|
|
|
115
|
-
operators = {"has", "like", "like!", "=", ">", "<", ">=", "<="}
|
|
201
|
+
operators = {"has", "like", "like!", "likep", "=", "!=", ">", "<", ">=", "<="}
|
|
116
202
|
|
|
117
203
|
queryOperations = {"sort", "limit", "count"}
|
|
118
204
|
|
|
@@ -1252,7 +1338,7 @@ def apply_query_operations(results, tokens, keys, positions=None):
|
|
|
1252
1338
|
return results
|
|
1253
1339
|
|
|
1254
1340
|
|
|
1255
|
-
functionMap = {"=": "__eq__", ">": "__gt__", "<": "__lt__", ">=": "__ge__", "<=": "__le__"}
|
|
1341
|
+
functionMap = {"=": "__eq__", "!=": "__ne__", ">": "__gt__", "<": "__lt__", ">=": "__ge__", "<=": "__le__"}
|
|
1256
1342
|
|
|
1257
1343
|
|
|
1258
1344
|
def convert_value(value, sample):
|
|
@@ -1296,14 +1382,35 @@ def parse_condition(tokens, index, source, keys, positions=None):
|
|
|
1296
1382
|
|
|
1297
1383
|
key = tokens[index]
|
|
1298
1384
|
operator = tokens[index + 1]
|
|
1299
|
-
|
|
1385
|
+
negated = operator.casefold() == "not"
|
|
1386
|
+
if negated:
|
|
1387
|
+
if index + 3 >= len(tokens):
|
|
1388
|
+
raise ChemQLError("Expected an operator and value after `not`", *(error_position(positions, index + 1) if positions else (None, 1))) # type: ignore
|
|
1389
|
+
operator = tokens[index + 2]
|
|
1390
|
+
value = tokens[index + 3]
|
|
1391
|
+
consumed = index + 4
|
|
1392
|
+
else:
|
|
1393
|
+
value = tokens[index + 2]
|
|
1394
|
+
consumed = index + 3
|
|
1300
1395
|
|
|
1301
1396
|
if key not in keys:
|
|
1302
1397
|
pos, length = error_position(positions, index) if positions else (None, 1)
|
|
1303
1398
|
raise ChemQLError(f"There is no key `{key}`", pos, length)
|
|
1304
1399
|
|
|
1305
|
-
if operator
|
|
1400
|
+
if operator in {"like", "like!", "likep"}:
|
|
1306
1401
|
pattern = value
|
|
1402
|
+
if operator == "likep":
|
|
1403
|
+
try:
|
|
1404
|
+
compiled_pattern = re.compile(pattern)
|
|
1405
|
+
except re.error as exc:
|
|
1406
|
+
pos, length = error_position(positions, index + 2) if positions else (None, 1)
|
|
1407
|
+
raise ChemQLError(f"Invalid regex: {exc}", pos, length) from exc
|
|
1408
|
+
|
|
1409
|
+
def regex_condition(item):
|
|
1410
|
+
actual = getattr(item, key)
|
|
1411
|
+
return isinstance(actual, str) and compiled_pattern.search(actual) is not None
|
|
1412
|
+
|
|
1413
|
+
return (lambda item, fn=regex_condition: not fn(item)) if negated else regex_condition, consumed
|
|
1307
1414
|
case_sensitive = operator == "like!"
|
|
1308
1415
|
|
|
1309
1416
|
def like_condition(item):
|
|
@@ -1318,7 +1425,7 @@ def parse_condition(tokens, index, source, keys, positions=None):
|
|
|
1318
1425
|
case_sensitive
|
|
1319
1426
|
)
|
|
1320
1427
|
|
|
1321
|
-
return
|
|
1428
|
+
return (lambda item, fn=like_condition: not fn(item)) if negated else like_condition, consumed
|
|
1322
1429
|
|
|
1323
1430
|
if operator == "has":
|
|
1324
1431
|
def has_condition(item):
|
|
@@ -1329,7 +1436,7 @@ def parse_condition(tokens, index, source, keys, positions=None):
|
|
|
1329
1436
|
except TypeError:
|
|
1330
1437
|
return False
|
|
1331
1438
|
|
|
1332
|
-
return
|
|
1439
|
+
return (lambda item, fn=has_condition: not fn(item)) if negated else has_condition, consumed
|
|
1333
1440
|
|
|
1334
1441
|
if operator not in functionMap:
|
|
1335
1442
|
pos, length = error_position(positions, index + 1) if positions else (None, 1)
|
|
@@ -1354,12 +1461,16 @@ def parse_condition(tokens, index, source, keys, positions=None):
|
|
|
1354
1461
|
except (TypeError, AttributeError):
|
|
1355
1462
|
return False
|
|
1356
1463
|
|
|
1357
|
-
return
|
|
1464
|
+
return (lambda item, fn=comparison_condition: not fn(item)) if negated else comparison_condition, consumed
|
|
1358
1465
|
|
|
1359
1466
|
def parse_primary(tokens, index, source, keys, positions=None):
|
|
1360
1467
|
if index >= len(tokens):
|
|
1361
1468
|
raise ChemQLError("Expected condition", *(error_position(positions, index) if positions else (None, 1)))#type:ignore
|
|
1362
1469
|
|
|
1470
|
+
if tokens[index].casefold() == "not":
|
|
1471
|
+
expression, next_index = parse_primary(tokens, index + 1, source, keys, positions)
|
|
1472
|
+
return (lambda item, fn=expression: not fn(item)), next_index
|
|
1473
|
+
|
|
1363
1474
|
if tokens[index] == "(":
|
|
1364
1475
|
expression, index = parse_or(
|
|
1365
1476
|
tokens,
|
|
@@ -1582,9 +1693,107 @@ def interpolate_python_queries(code):
|
|
|
1582
1693
|
python_globals.update(query_values)
|
|
1583
1694
|
return "".join(rewritten)
|
|
1584
1695
|
|
|
1696
|
+
def _space_query_operators(text):
|
|
1697
|
+
"""Separate comparison operators and parentheses outside quoted strings."""
|
|
1698
|
+
out, quote, escaped = [], None, False
|
|
1699
|
+
i = 0
|
|
1700
|
+
while i < len(text):
|
|
1701
|
+
ch = text[i]
|
|
1702
|
+
if quote:
|
|
1703
|
+
out.append(ch)
|
|
1704
|
+
if escaped:
|
|
1705
|
+
escaped = False
|
|
1706
|
+
elif ch == "\\":
|
|
1707
|
+
escaped = True
|
|
1708
|
+
elif ch == quote:
|
|
1709
|
+
quote = None
|
|
1710
|
+
i += 1
|
|
1711
|
+
continue
|
|
1712
|
+
if ch in ("'", '"'):
|
|
1713
|
+
quote = ch
|
|
1714
|
+
out.append(ch)
|
|
1715
|
+
i += 1
|
|
1716
|
+
continue
|
|
1717
|
+
if ch in "()":
|
|
1718
|
+
out.extend((" ", ch, " "))
|
|
1719
|
+
i += 1
|
|
1720
|
+
continue
|
|
1721
|
+
if ch in "<>!=" and i + 1 < len(text) and text[i + 1] == "=":
|
|
1722
|
+
out.extend((" ", ch + "=", " "))
|
|
1723
|
+
i += 2
|
|
1724
|
+
continue
|
|
1725
|
+
if ch in "<>!=":
|
|
1726
|
+
out.extend((" ", ch, " "))
|
|
1727
|
+
i += 1
|
|
1728
|
+
continue
|
|
1729
|
+
out.append(ch)
|
|
1730
|
+
i += 1
|
|
1731
|
+
return "".join(out)
|
|
1732
|
+
|
|
1733
|
+
|
|
1734
|
+
def _split_set_operations(text):
|
|
1735
|
+
"""Split top-level union/intersection outside quotes and parentheses, left-to-right."""
|
|
1736
|
+
parts, ops, start, depth = [], [], 0, 0
|
|
1737
|
+
quote, escaped = None, False
|
|
1738
|
+
words = re.finditer(r"\b(union|intersection)\b", text, re.IGNORECASE)
|
|
1739
|
+
for match in words:
|
|
1740
|
+
i = match.start()
|
|
1741
|
+
quote = None
|
|
1742
|
+
escaped = False
|
|
1743
|
+
depth = 0
|
|
1744
|
+
j = 0
|
|
1745
|
+
while j < i:
|
|
1746
|
+
ch = text[j]
|
|
1747
|
+
if quote:
|
|
1748
|
+
if escaped: escaped = False
|
|
1749
|
+
elif ch == "\\": escaped = True
|
|
1750
|
+
elif ch == quote: quote = None
|
|
1751
|
+
elif ch in ("'", '"'): quote = ch
|
|
1752
|
+
elif ch == "(": depth += 1
|
|
1753
|
+
elif ch == ")": depth -= 1
|
|
1754
|
+
j += 1
|
|
1755
|
+
if quote is None and depth == 0:
|
|
1756
|
+
parts.append(text[start:i].strip())
|
|
1757
|
+
ops.append(match.group(1).casefold())
|
|
1758
|
+
start = match.end()
|
|
1759
|
+
if not ops:
|
|
1760
|
+
return None
|
|
1761
|
+
parts.append(text[start:].strip())
|
|
1762
|
+
if any(not part for part in parts):
|
|
1763
|
+
raise ChemQLError("Missing query around union/intersection")
|
|
1764
|
+
return parts, ops
|
|
1765
|
+
|
|
1766
|
+
|
|
1767
|
+
def _as_result_list(result):
|
|
1768
|
+
if result is None or result == "No matching item":
|
|
1769
|
+
return []
|
|
1770
|
+
return result if isinstance(result, list) else [result]
|
|
1771
|
+
|
|
1772
|
+
|
|
1585
1773
|
def execute_query_text(text):
|
|
1586
1774
|
"""Execute plain query-language text."""
|
|
1587
|
-
text =
|
|
1775
|
+
text = text.strip()
|
|
1776
|
+
set_expression = _split_set_operations(text)
|
|
1777
|
+
if set_expression:
|
|
1778
|
+
parts, ops = set_expression
|
|
1779
|
+
result = _as_result_list(execute_query_text(parts[0]))
|
|
1780
|
+
for operator, part in zip(ops, parts[1:]):
|
|
1781
|
+
right = _as_result_list(execute_query_text(part))
|
|
1782
|
+
def identity(item):
|
|
1783
|
+
if hasattr(item, "id"): return ("id", getattr(item, "id"))
|
|
1784
|
+
if hasattr(item, "number"): return ("number", getattr(item, "number"))
|
|
1785
|
+
if hasattr(item, "cid") and getattr(item, "cid") is not None: return ("cid", getattr(item, "cid"))
|
|
1786
|
+
return (type(item).__name__, repr(item))
|
|
1787
|
+
right_map = {identity(item): item for item in right}
|
|
1788
|
+
if operator == "union":
|
|
1789
|
+
seen = {identity(item) for item in result}
|
|
1790
|
+
result.extend(item for item in right if identity(item) not in seen and not seen.add(identity(item)))
|
|
1791
|
+
else:
|
|
1792
|
+
result = [item for item in result if identity(item) in right_map]
|
|
1793
|
+
return result
|
|
1794
|
+
|
|
1795
|
+
text = interpolate_query(text)
|
|
1796
|
+
text = _space_query_operators(text)
|
|
1588
1797
|
positions = token_positions(text)
|
|
1589
1798
|
|
|
1590
1799
|
try:
|
|
@@ -1968,6 +2177,12 @@ def process_inline(text):
|
|
|
1968
2177
|
if not stripped[end + 2:].strip():
|
|
1969
2178
|
return execute_query_block(stripped[2:end].strip())
|
|
1970
2179
|
|
|
2180
|
+
# Backtick-wrapped commands use the persistent Bash session.
|
|
2181
|
+
if len(stripped) >= 2 and stripped.startswith("`") and stripped.endswith("`"):
|
|
2182
|
+
command = stripped[1:-1]
|
|
2183
|
+
return run_shell_command(command)
|
|
2184
|
+
|
|
2185
|
+
|
|
1971
2186
|
# Everything else is query language.
|
|
1972
2187
|
return execute_query_text(stripped)
|
|
1973
2188
|
|
|
@@ -2099,12 +2314,13 @@ def parse_temperature_setting(value):
|
|
|
2099
2314
|
if normalized == "room":
|
|
2100
2315
|
return 298.15
|
|
2101
2316
|
|
|
2102
|
-
match = re.fullmatch(r"([+-]?(?:\d+(?:\.\d*)?|\.\d+))\s
|
|
2317
|
+
match = re.fullmatch(r"([+-]?(?:\d+(?:\.\d*)?|\.\d+))\s*(?:°\s*)?(celsius|fahrenheit|kelvin|degc|degf|degk|c|f|k)", normalized)
|
|
2103
2318
|
if not match:
|
|
2104
2319
|
raise ValueError("Temperature must use C, F, or K, or be `standard` or `room`")
|
|
2105
2320
|
|
|
2106
2321
|
temperature = float(match.group(1))
|
|
2107
2322
|
unit = match.group(2)
|
|
2323
|
+
unit = {"celsius": "c", "degc": "c", "fahrenheit": "f", "degf": "f", "kelvin": "k", "degk": "k", "K":"k", "C":"c", "F": "f","k":"k", "c":"c", "f": "f"}[unit]
|
|
2108
2324
|
if unit == "f":
|
|
2109
2325
|
temperature = (temperature - 32) * 5 / 9 + 273.15
|
|
2110
2326
|
elif unit == "c":
|
|
@@ -2130,14 +2346,16 @@ def parse_pressure_setting(value):
|
|
|
2130
2346
|
pressure = float(match.group(1))
|
|
2131
2347
|
unit = re.sub(r"\s+", "", match.group(2)).replace("²", "^2")
|
|
2132
2348
|
unit_scales = {
|
|
2133
|
-
"pa": 1.0,
|
|
2134
|
-
"
|
|
2135
|
-
"
|
|
2136
|
-
"
|
|
2137
|
-
"
|
|
2349
|
+
"pa": 1.0, "pascal": 1.0, "pascals": 1.0,
|
|
2350
|
+
"kpa": 1_000.0, "mpa": 1_000_000.0,
|
|
2351
|
+
"hpa": 100.0, "mbar": 100.0,
|
|
2352
|
+
"n/m^2": 1.0, "n/m2": 1.0, "nm^-2": 1.0, "n·m^-2": 1.0,
|
|
2353
|
+
"bar": 100_000.0, "mbar": 100.0,
|
|
2354
|
+
"atm": 101_325.0, "torr": 101_325.0 / 760, "mmhg": 101_325.0 / 760,
|
|
2355
|
+
"psi": 6894.757293168, "kpsi": 6_894_757.293168,
|
|
2138
2356
|
}
|
|
2139
2357
|
if unit not in unit_scales:
|
|
2140
|
-
raise ValueError("
|
|
2358
|
+
raise ValueError("Unsupported pressure unit. Try Pa, kPa, MPa, hPa, bar, atm, torr, mmHg, or psi")
|
|
2141
2359
|
|
|
2142
2360
|
return pressure * unit_scales[unit]
|
|
2143
2361
|
|
|
@@ -2550,6 +2768,8 @@ def execute(arguments, query_text=None, positions=None):
|
|
|
2550
2768
|
)
|
|
2551
2769
|
|
|
2552
2770
|
source, keys = SOURCES[source_name]
|
|
2771
|
+
if i >= len(arguments):
|
|
2772
|
+
return list(source)
|
|
2553
2773
|
return_attributes = None
|
|
2554
2774
|
|
|
2555
2775
|
if "return" in arguments[i:]:
|
|
@@ -2941,6 +3161,18 @@ class ChemqlLexer(Lexer):
|
|
|
2941
3161
|
|
|
2942
3162
|
i = end + 2
|
|
2943
3163
|
continue
|
|
3164
|
+
|
|
3165
|
+
if line.startswith("//", i):
|
|
3166
|
+
|
|
3167
|
+
end = len(line)
|
|
3168
|
+
|
|
3169
|
+
tokens.append((
|
|
3170
|
+
"class:comment",
|
|
3171
|
+
line[i:end]
|
|
3172
|
+
))
|
|
3173
|
+
|
|
3174
|
+
i = end
|
|
3175
|
+
continue
|
|
2944
3176
|
|
|
2945
3177
|
if line[i] == "[":
|
|
2946
3178
|
end = line.find("]", i + 1)
|
|
@@ -2955,6 +3187,20 @@ class ChemqlLexer(Lexer):
|
|
|
2955
3187
|
|
|
2956
3188
|
i = end + 1
|
|
2957
3189
|
continue
|
|
3190
|
+
|
|
3191
|
+
if line[i] == "`":
|
|
3192
|
+
end = line.find("`", i + 1)
|
|
3193
|
+
|
|
3194
|
+
if end == -1:
|
|
3195
|
+
end = len(line) - 1
|
|
3196
|
+
|
|
3197
|
+
tokens.append((
|
|
3198
|
+
"class:terminalcommand",
|
|
3199
|
+
line[i:end + 1]
|
|
3200
|
+
))
|
|
3201
|
+
|
|
3202
|
+
i = end + 1
|
|
3203
|
+
continue
|
|
2958
3204
|
|
|
2959
3205
|
# Strings
|
|
2960
3206
|
if line[i] in "\"'":
|
|
@@ -2983,7 +3229,7 @@ class ChemqlLexer(Lexer):
|
|
|
2983
3229
|
# Operators
|
|
2984
3230
|
matched_operator = None
|
|
2985
3231
|
|
|
2986
|
-
for operator in (">=", "<=", "=", ">", "<"):
|
|
3232
|
+
for operator in ("!=", ">=", "<=", "=", ">", "<"):
|
|
2987
3233
|
if line.startswith(operator, i):
|
|
2988
3234
|
matched_operator = operator
|
|
2989
3235
|
break
|
|
@@ -3040,7 +3286,9 @@ class ChemqlLexer(Lexer):
|
|
|
3040
3286
|
"conditions",
|
|
3041
3287
|
"view",
|
|
3042
3288
|
"help",
|
|
3043
|
-
"dump"
|
|
3289
|
+
"dump",
|
|
3290
|
+
"union",
|
|
3291
|
+
"intersection",
|
|
3044
3292
|
}:
|
|
3045
3293
|
style = "class:command"
|
|
3046
3294
|
|
|
@@ -3058,6 +3306,7 @@ class ChemqlLexer(Lexer):
|
|
|
3058
3306
|
"has",
|
|
3059
3307
|
"like",
|
|
3060
3308
|
"like!",
|
|
3309
|
+
"likep",
|
|
3061
3310
|
"sort",
|
|
3062
3311
|
"limit",
|
|
3063
3312
|
"count",
|
|
@@ -3152,7 +3401,7 @@ class ChemqlCompleter(Completer):
|
|
|
3152
3401
|
if source_name in SOURCES:
|
|
3153
3402
|
_, keys = SOURCES[source_name]
|
|
3154
3403
|
suggestions = list(keys) + [
|
|
3155
|
-
"=", ">", "<", ">=", "<=", "like", "like!", "has",
|
|
3404
|
+
"=", ">", "<", ">=", "<=", "!=", "like", "like!", "likep", "has",
|
|
3156
3405
|
"sort", "limit", "count", "asc", "desc", "return",
|
|
3157
3406
|
]
|
|
3158
3407
|
|
|
@@ -3167,6 +3416,8 @@ class ChemqlCompleter(Completer):
|
|
|
3167
3416
|
|
|
3168
3417
|
chemql_style = Style.from_dict({
|
|
3169
3418
|
"command": "ansicyan",
|
|
3419
|
+
"terminalcommand": "ansipurple",
|
|
3420
|
+
"comment": "#545454 bold underline",
|
|
3170
3421
|
"source": "ansiblue",
|
|
3171
3422
|
"key": "ansimagenta",
|
|
3172
3423
|
"operator": "ansiyellow",
|
|
@@ -3178,6 +3429,138 @@ chemql_style = Style.from_dict({
|
|
|
3178
3429
|
})
|
|
3179
3430
|
|
|
3180
3431
|
|
|
3432
|
+
|
|
3433
|
+
|
|
3434
|
+
class _ChemQLField:
|
|
3435
|
+
"""Field selector used by the fluent Python query API."""
|
|
3436
|
+
def __init__(self, query, field):
|
|
3437
|
+
self.query = query
|
|
3438
|
+
self.field = field
|
|
3439
|
+
|
|
3440
|
+
def condition(self, **conditions):
|
|
3441
|
+
operators = {
|
|
3442
|
+
"eq": "=", "ne": "!=", "lt": "<", "lte": "<=",
|
|
3443
|
+
"gt": ">", "gte": ">=", "like": "like", "likep": "likep",
|
|
3444
|
+
"has": "has",
|
|
3445
|
+
}
|
|
3446
|
+
if len(conditions) != 1:
|
|
3447
|
+
raise ValueError("condition() expects exactly one condition, e.g. lt=10")
|
|
3448
|
+
name, value = next(iter(conditions.items()))
|
|
3449
|
+
if name not in operators:
|
|
3450
|
+
raise ValueError(f"Unknown condition {name!r}; use {', '.join(operators)}")
|
|
3451
|
+
rendered = shlex.quote(str(value))
|
|
3452
|
+
return ChemQLQuery(f"{self.query.query} {self.field} {operators[name]} {rendered}")
|
|
3453
|
+
|
|
3454
|
+
|
|
3455
|
+
class ChemQLQuery:
|
|
3456
|
+
"""Composable ChemQL query. Use .execute() to evaluate it."""
|
|
3457
|
+
def __init__(self, query):
|
|
3458
|
+
self.query = str(query).strip()
|
|
3459
|
+
|
|
3460
|
+
def __getattr__(self, field):
|
|
3461
|
+
if field.startswith("_"):
|
|
3462
|
+
raise AttributeError(field)
|
|
3463
|
+
return _ChemQLField(self, field)
|
|
3464
|
+
|
|
3465
|
+
def return_(self, *attributes):
|
|
3466
|
+
if not attributes:
|
|
3467
|
+
raise ValueError("return_() requires at least one attribute")
|
|
3468
|
+
return ChemQLQuery(f"{self.query} return " + " ".join(attributes))
|
|
3469
|
+
|
|
3470
|
+
def execute(self):
|
|
3471
|
+
return execute_query_text(self.query)
|
|
3472
|
+
|
|
3473
|
+
def __or__(self, other):
|
|
3474
|
+
if not isinstance(other, ChemQLQuery):
|
|
3475
|
+
return NotImplemented
|
|
3476
|
+
return ChemQLQuery(f"{self.query} union {other.query}")
|
|
3477
|
+
|
|
3478
|
+
def __and__(self, other):
|
|
3479
|
+
if not isinstance(other, ChemQLQuery):
|
|
3480
|
+
return NotImplemented
|
|
3481
|
+
return ChemQLQuery(f"{self.query} intersection {other.query}")
|
|
3482
|
+
|
|
3483
|
+
def __str__(self):
|
|
3484
|
+
return f"Query of `{self.query}`"
|
|
3485
|
+
|
|
3486
|
+
def __repr__(self):
|
|
3487
|
+
return str(self)
|
|
3488
|
+
|
|
3489
|
+
|
|
3490
|
+
def search(source):
|
|
3491
|
+
"""Create a native Python API query, e.g. search('elements')."""
|
|
3492
|
+
source = str(source).strip()
|
|
3493
|
+
if source not in SOURCES:
|
|
3494
|
+
raise ValueError(f"Unknown ChemQL source {source!r}; expected one of {', '.join(SOURCES)}")
|
|
3495
|
+
return ChemQLQuery(f"search {source}")
|
|
3496
|
+
|
|
3497
|
+
|
|
3498
|
+
def union(x, y):
|
|
3499
|
+
"""Return the left-to-right union of two ChemQLQuery objects."""
|
|
3500
|
+
if not isinstance(x, ChemQLQuery) or not isinstance(y, ChemQLQuery):
|
|
3501
|
+
raise TypeError("union() expects two ChemQLQuery objects")
|
|
3502
|
+
return x | y
|
|
3503
|
+
|
|
3504
|
+
|
|
3505
|
+
def intersection(x, y):
|
|
3506
|
+
"""Return the intersection of two ChemQLQuery objects."""
|
|
3507
|
+
if not isinstance(x, ChemQLQuery) or not isinstance(y, ChemQLQuery):
|
|
3508
|
+
raise TypeError("intersection() expects two ChemQLQuery objects")
|
|
3509
|
+
return x & y
|
|
3510
|
+
|
|
3511
|
+
PACKAGE_NAME = "chemql-python"
|
|
3512
|
+
|
|
3513
|
+
def upgrade_package(version=None):
|
|
3514
|
+
"""Upgrade ChemQL, optionally to a specific version."""
|
|
3515
|
+
|
|
3516
|
+
if version is not None:
|
|
3517
|
+
# Accept ordinary numeric release versions, optionally with
|
|
3518
|
+
# prerelease or post-release suffixes.
|
|
3519
|
+
if not re.fullmatch(
|
|
3520
|
+
r"\d+(?:\.\d+)*(?:(?:a|b|rc)\d+)?(?:\.post\d+)?(?:\.dev\d+)?",
|
|
3521
|
+
version,
|
|
3522
|
+
re.IGNORECASE,
|
|
3523
|
+
):
|
|
3524
|
+
print(f"Invalid version: {version}", file=sys.stderr)
|
|
3525
|
+
return 2
|
|
3526
|
+
|
|
3527
|
+
target = f"{PACKAGE_NAME}=={version}"
|
|
3528
|
+
print(f"Installing {target}...")
|
|
3529
|
+
cmd = [
|
|
3530
|
+
sys.executable, "-m", "pip", "install",
|
|
3531
|
+
"--force-reinstall", target,
|
|
3532
|
+
]
|
|
3533
|
+
else:
|
|
3534
|
+
print(f"Upgrading {PACKAGE_NAME} to the latest version...")
|
|
3535
|
+
cmd = [
|
|
3536
|
+
sys.executable, "-m", "pip", "install",
|
|
3537
|
+
"--upgrade", PACKAGE_NAME,
|
|
3538
|
+
]
|
|
3539
|
+
|
|
3540
|
+
try:
|
|
3541
|
+
result = subprocess.run(cmd, check=False)
|
|
3542
|
+
if result.returncode != 0:
|
|
3543
|
+
print(
|
|
3544
|
+
f"Installation failed (exit code {result.returncode}).",
|
|
3545
|
+
file=sys.stderr,
|
|
3546
|
+
)
|
|
3547
|
+
return result.returncode
|
|
3548
|
+
|
|
3549
|
+
except KeyboardInterrupt:
|
|
3550
|
+
print("\nUpgrade cancelled.", file=sys.stderr)
|
|
3551
|
+
return 130
|
|
3552
|
+
except OSError as exc:
|
|
3553
|
+
print(f"Could not run pip: {exc}", file=sys.stderr)
|
|
3554
|
+
return 1
|
|
3555
|
+
|
|
3556
|
+
print("ChemQL installation completed.")
|
|
3557
|
+
return 0
|
|
3558
|
+
|
|
3559
|
+
|
|
3560
|
+
ChemQLQuery.__name__ = "ChemQLQuery"
|
|
3561
|
+
ChemQLQuery.__qualname__ = "ChemQLQuery"
|
|
3562
|
+
|
|
3563
|
+
|
|
3181
3564
|
def main():
|
|
3182
3565
|
parser = argparse.ArgumentParser(
|
|
3183
3566
|
description="Chemql query and chemistry language"
|
|
@@ -3199,19 +3582,40 @@ def main():
|
|
|
3199
3582
|
"-v",
|
|
3200
3583
|
"--version",
|
|
3201
3584
|
action="version",
|
|
3202
|
-
version="0.
|
|
3585
|
+
version="1.0.1",
|
|
3203
3586
|
)
|
|
3204
3587
|
|
|
3588
|
+
|
|
3589
|
+
parser.add_argument(
|
|
3590
|
+
"--upgrade",
|
|
3591
|
+
nargs="?",
|
|
3592
|
+
const="latest",
|
|
3593
|
+
default=None,
|
|
3594
|
+
metavar="VERSION",
|
|
3595
|
+
help="Upgrade ChemQL, optionally to a specific version.",
|
|
3596
|
+
)
|
|
3597
|
+
|
|
3598
|
+
|
|
3205
3599
|
args = parser.parse_args()
|
|
3206
3600
|
|
|
3207
|
-
if args.
|
|
3208
|
-
|
|
3601
|
+
if args.upgrade is not None:
|
|
3602
|
+
version_target = (
|
|
3603
|
+
None if args.upgrade == "latest" else args.upgrade
|
|
3604
|
+
)
|
|
3605
|
+
sys.exit(upgrade_package(version_target))
|
|
3606
|
+
|
|
3209
3607
|
|
|
3210
3608
|
elif args.file:
|
|
3211
3609
|
with open(args.file) as f:
|
|
3212
3610
|
code = f.read()
|
|
3213
3611
|
process_lines(code.splitlines())
|
|
3214
3612
|
|
|
3613
|
+
elif not sys.stdin.isatty():
|
|
3614
|
+
piped = sys.stdin.read()
|
|
3615
|
+
if piped.strip():
|
|
3616
|
+
process_lines([piped.strip()])
|
|
3617
|
+
else:
|
|
3618
|
+
run_interactive()
|
|
3215
3619
|
else:
|
|
3216
3620
|
run_interactive()
|
|
3217
3621
|
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chemql-python
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: A chemistry-focused query language and REPL
|
|
5
|
+
Author-email: Mohammad Rakibul Islam <rakibulofficial1017@gmail.com>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: prompt_toolkit>=3.0
|
|
10
|
+
Provides-Extra: test
|
|
11
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
# ChemQL
|
|
15
|
+
|
|
16
|
+
**ChemQL** is a chemistry-focused query language for searching and
|
|
17
|
+
working with chemical data from Python or an interactive terminal. It
|
|
18
|
+
provides a compact way to query elements, molecules, and chemical
|
|
19
|
+
reactions using filters, logical conditions, sorting, and result
|
|
20
|
+
projections.
|
|
21
|
+
|
|
22
|
+
> Package: `chemql-python`\
|
|
23
|
+
> Python import: `chemql`
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
- Query element, molecule, and reaction datasets.
|
|
28
|
+
- Filter records with comparisons and logical conditions.
|
|
29
|
+
- Match text using wildcard patterns with `like` and case-sensitive
|
|
30
|
+
`like!`.
|
|
31
|
+
- Use `and`, `or`, `not`, and parentheses to combine conditions.
|
|
32
|
+
- Use `likep` for Python regular-expression matching.
|
|
33
|
+
- Combine list results with `union` and `intersection`.
|
|
34
|
+
- Sort, limit, count, and project results with `return`.
|
|
35
|
+
- Look up elements, molecules, and reactions using `findel`,
|
|
36
|
+
`findmol`, and `findre`.
|
|
37
|
+
- Balance chemical equations.
|
|
38
|
+
- Set reaction conditions, including temperature, pressure, and
|
|
39
|
+
catalysts.
|
|
40
|
+
- Use Python expressions and control blocks alongside ChemQL queries.
|
|
41
|
+
- Use ChemQL from the command line or through its Python API.
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
Install the Python package from PyPI:
|
|
46
|
+
|
|
47
|
+
``` bash
|
|
48
|
+
pip install chemql-python
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Upgrading the package or switching versions
|
|
52
|
+
|
|
53
|
+
If your chemql-python version is >=1.0.1, you can simply run
|
|
54
|
+
```bash
|
|
55
|
+
chemql --upgrade
|
|
56
|
+
```
|
|
57
|
+
Or to switch versions to for example 0.2.2
|
|
58
|
+
```bash
|
|
59
|
+
chemql --upgrade 0.2.2
|
|
60
|
+
```
|
|
61
|
+
However if you do not have chemql-python version >=1.0.1
|
|
62
|
+
```bash
|
|
63
|
+
pip install --upgrade chemql-python
|
|
64
|
+
```
|
|
65
|
+
Or to switch versions to for example 0.2.2
|
|
66
|
+
```bash
|
|
67
|
+
pip install --force-reinstall chemql-python==0.2.2
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Quick start
|
|
71
|
+
|
|
72
|
+
Start the interactive terminal:
|
|
73
|
+
|
|
74
|
+
``` bash
|
|
75
|
+
chemql
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Then try:
|
|
79
|
+
|
|
80
|
+
``` text
|
|
81
|
+
search elements name = "Hydrogen"
|
|
82
|
+
findel "He"
|
|
83
|
+
findmol "H2O"
|
|
84
|
+
search molecules molecular_weight < 20 return name formula molecular_weight
|
|
85
|
+
search reactions reaction_type = "combustion" return name equation
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Show the built-in command reference from the REPL:
|
|
89
|
+
|
|
90
|
+
``` text
|
|
91
|
+
help
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Command-line usage
|
|
95
|
+
|
|
96
|
+
Run a single command with `-c`:
|
|
97
|
+
|
|
98
|
+
``` bash
|
|
99
|
+
chemql -c 'search elements name = "Hydrogen"'
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Run commands from a file with `-f`:
|
|
103
|
+
|
|
104
|
+
``` bash
|
|
105
|
+
chemql -f queries.chemql
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Check the installed version:
|
|
109
|
+
|
|
110
|
+
``` bash
|
|
111
|
+
chemql --version
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
A query file can contain multiple ChemQL commands, one per line. Use the
|
|
115
|
+
`//` comment marker for comments and a trailing backslash (`\`) to
|
|
116
|
+
continue a command onto the next line.
|
|
117
|
+
|
|
118
|
+
Example `queries.chemql`:
|
|
119
|
+
|
|
120
|
+
``` text
|
|
121
|
+
// Find lighter molecules
|
|
122
|
+
search molecules molecular_weight < 20 return name formula molecular_weight
|
|
123
|
+
|
|
124
|
+
// Show a subset of the periodic table
|
|
125
|
+
search elements atomic_number > 10 and atomic_number < 20 sort atomic_number desc limit 5
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Query language
|
|
129
|
+
|
|
130
|
+
### Data sources
|
|
131
|
+
|
|
132
|
+
ChemQL provides three main sources:
|
|
133
|
+
|
|
134
|
+
Source Contents
|
|
135
|
+
------------- ----------------------------------------------
|
|
136
|
+
`elements` Periodic-table element data
|
|
137
|
+
`molecules` Molecule properties and identifiers
|
|
138
|
+
`reactions` Reaction equations, conditions, and metadata
|
|
139
|
+
|
|
140
|
+
Search a source explicitly:
|
|
141
|
+
|
|
142
|
+
``` text
|
|
143
|
+
search elements
|
|
144
|
+
search molecules
|
|
145
|
+
search reactions
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
You can also use `search all` before a source name, for example:
|
|
149
|
+
|
|
150
|
+
``` text
|
|
151
|
+
search all elements atomic_mass < 100
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Change the active source in an interactive session with:
|
|
155
|
+
|
|
156
|
+
``` text
|
|
157
|
+
source elements
|
|
158
|
+
source molecules
|
|
159
|
+
source reactions
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Filtering
|
|
163
|
+
|
|
164
|
+
Use comparison operators to filter records:
|
|
165
|
+
|
|
166
|
+
Operator Meaning
|
|
167
|
+
---------- ----------------------------------------
|
|
168
|
+
`=` Equal
|
|
169
|
+
`!=` Not equal
|
|
170
|
+
`>` Greater than
|
|
171
|
+
`<` Less than
|
|
172
|
+
`>=` Greater than or equal
|
|
173
|
+
`<=` Less than or equal
|
|
174
|
+
`like` Case-insensitive wildcard match
|
|
175
|
+
`like!` Case-sensitive wildcard match
|
|
176
|
+
`likep` Python regular-expression match
|
|
177
|
+
`has` Check whether a value contains an item
|
|
178
|
+
|
|
179
|
+
Examples:
|
|
180
|
+
|
|
181
|
+
``` text
|
|
182
|
+
search elements atomic_number > 10
|
|
183
|
+
search molecules molecular_weight <= 20
|
|
184
|
+
search molecules name like "*water*"
|
|
185
|
+
search reactions id likep ".*haber(\s*|-|_)process.*"
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The wildcard matcher supports `*` for a sequence of characters and other
|
|
189
|
+
wildcard tokens supported by ChemQL's pattern matcher. Use `likep` when
|
|
190
|
+
you need regular expressions rather than wildcard patterns.
|
|
191
|
+
|
|
192
|
+
### Logical conditions and parentheses
|
|
193
|
+
|
|
194
|
+
Combine conditions with `and`, `or`, and `not`. Parentheses group
|
|
195
|
+
conditions. Comparison operators and parentheses can be written without
|
|
196
|
+
surrounding spaces.
|
|
197
|
+
|
|
198
|
+
``` text
|
|
199
|
+
search elements name not like "*ium"
|
|
200
|
+
search elements name not like "*ium" and not atomic_mass < 50
|
|
201
|
+
search elements atomic_mass > 50 and (density<10 or boil<100)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Combining query results
|
|
205
|
+
|
|
206
|
+
Use `union` to combine results and `intersection` to keep records
|
|
207
|
+
present in both result lists. Operations are evaluated from left to
|
|
208
|
+
right.
|
|
209
|
+
|
|
210
|
+
``` text
|
|
211
|
+
search elements name not like "*ium" intersection search elements name like "*um"
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Use the Python API's query-object operators `|` and `&` for union and
|
|
215
|
+
intersection, respectively, when working with `ChemQLQuery` objects.
|
|
216
|
+
|
|
217
|
+
### Sorting, limiting, counting, and returning fields
|
|
218
|
+
|
|
219
|
+
``` text
|
|
220
|
+
search elements sort atomic_number
|
|
221
|
+
search molecules sort molecular_weight desc
|
|
222
|
+
search molecules limit 10
|
|
223
|
+
search molecules count
|
|
224
|
+
search molecules molecular_weight < 20 return name
|
|
225
|
+
search molecules molecular_weight < 20 return name formula molecular_weight
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
- `sort <field> [asc|desc]` sorts results.
|
|
229
|
+
- `limit <number>` limits the number of results.
|
|
230
|
+
- `count` returns the number of matches.
|
|
231
|
+
- `return <field...>` returns only selected fields. One field produces
|
|
232
|
+
a list; multiple fields produce a table-like result.
|
|
233
|
+
- `first` and `last` can be used to select one result.
|
|
234
|
+
|
|
235
|
+
### Find commands
|
|
236
|
+
|
|
237
|
+
``` text
|
|
238
|
+
findel "He"
|
|
239
|
+
findel "Hydrogen"
|
|
240
|
+
|
|
241
|
+
findmol "H2O"
|
|
242
|
+
findmol "Water"
|
|
243
|
+
|
|
244
|
+
findre "Haber process"
|
|
245
|
+
findre "RHEA:10000" return name equation source
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
`findmol` searches the local molecule dataset first and can use external
|
|
249
|
+
chemical-data services when a local match is not found. External lookups
|
|
250
|
+
require an internet connection and may be unavailable if the service is
|
|
251
|
+
down.
|
|
252
|
+
|
|
253
|
+
## Reactions and equation balancing
|
|
254
|
+
|
|
255
|
+
ChemQL includes reaction data and commands for working with reaction
|
|
256
|
+
conditions.
|
|
257
|
+
|
|
258
|
+
``` text
|
|
259
|
+
findre "Haber process"
|
|
260
|
+
balance O2 + H2 -> H2O
|
|
261
|
+
balance O2 + H2 <-> H2O
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
The `->` arrow denotes an irreversible reaction, while `<->` denotes a
|
|
265
|
+
reversible reaction.
|
|
266
|
+
|
|
267
|
+
Set conditions before using `react`:
|
|
268
|
+
|
|
269
|
+
``` text
|
|
270
|
+
set temperature 723K
|
|
271
|
+
set pressure 20265000pa
|
|
272
|
+
set catalysts iron zinc
|
|
273
|
+
conditions
|
|
274
|
+
react N2 3H2
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Temperature can be supplied in Celsius (`C`), Fahrenheit (`F`), or
|
|
278
|
+
Kelvin (`K`). Pressure supports units such as pascals and bar; the
|
|
279
|
+
available unit conversions depend on the installed version. Preset
|
|
280
|
+
conditions are also available:
|
|
281
|
+
|
|
282
|
+
``` text
|
|
283
|
+
set temperature standard
|
|
284
|
+
set pressure room
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Use `help` in the terminal for the command reference included with your
|
|
288
|
+
installed version.
|
|
289
|
+
|
|
290
|
+
## Python API
|
|
291
|
+
|
|
292
|
+
Import ChemQL from Python:
|
|
293
|
+
|
|
294
|
+
``` python
|
|
295
|
+
import chemql
|
|
296
|
+
|
|
297
|
+
result = chemql.execute_query_text(
|
|
298
|
+
'search elements name = "Hydrogen"'
|
|
299
|
+
)
|
|
300
|
+
print(result)
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
You can also use `execute_query` for a query string:
|
|
304
|
+
|
|
305
|
+
``` python
|
|
306
|
+
import chemql
|
|
307
|
+
|
|
308
|
+
result = chemql.execute_query('search molecules molecular_weight < 20')
|
|
309
|
+
print(result)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
The package exposes chemistry data classes and helpers, including
|
|
313
|
+
`Element`, `Molecule`, `Reaction`, `ChemQLError`, `Unknown`,
|
|
314
|
+
`ReturnTable`, and `balance_stoichiometry`.
|
|
315
|
+
|
|
316
|
+
### Query builder API
|
|
317
|
+
|
|
318
|
+
Versions that include the query-builder API also support chaining query
|
|
319
|
+
conditions:
|
|
320
|
+
|
|
321
|
+
``` python
|
|
322
|
+
import chemql
|
|
323
|
+
|
|
324
|
+
x = chemql.search("elements")
|
|
325
|
+
y = x.atomic_mass.condition(lt=10)
|
|
326
|
+
z = y.return_("name")
|
|
327
|
+
|
|
328
|
+
print(z) # The query as text
|
|
329
|
+
print(z.execute()) # Execute the query
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Query objects can be combined with `|` (union) and `&` (intersection).
|
|
333
|
+
The convenience functions `chemql.union(x, y)` and
|
|
334
|
+
`chemql.intersection(x, y)` are also available in versions that include
|
|
335
|
+
this API.
|
|
336
|
+
|
|
337
|
+
## Hybrid Python
|
|
338
|
+
|
|
339
|
+
ChemQL can execute Python expressions inside square brackets:
|
|
340
|
+
|
|
341
|
+
``` text
|
|
342
|
+
[x = 10]
|
|
343
|
+
[print(x)]
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
It also supports Python control blocks using `[for ...]`, `[if ...]`,
|
|
347
|
+
`[elif ...]`, `[else]`, `[while ...]`, `[break]`, `[continue]`, and
|
|
348
|
+
`[endblock]`.
|
|
349
|
+
|
|
350
|
+
Use `{{ ... }}` to evaluate a ChemQL query and insert its result into
|
|
351
|
+
Python:
|
|
352
|
+
|
|
353
|
+
``` text
|
|
354
|
+
[print({{ search elements atomic_number = 1 }})]
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Only run Python code and shell commands from sources you trust.
|
|
358
|
+
|
|
359
|
+
## Shell Commands
|
|
360
|
+
|
|
361
|
+
ChemQL supports executing system shell commands by enclosing them in backticks (`` ` ``). Commands execute in a **persistent shell session**, allowing shell state to be preserved between commands.
|
|
362
|
+
|
|
363
|
+
**Syntax**
|
|
364
|
+
|
|
365
|
+
```chemql
|
|
366
|
+
`command`
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
**Examples**
|
|
370
|
+
|
|
371
|
+
Execute system commands:
|
|
372
|
+
|
|
373
|
+
```chemql
|
|
374
|
+
`whoami`
|
|
375
|
+
`dir`
|
|
376
|
+
`echo Hello`
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Change the working directory:
|
|
380
|
+
|
|
381
|
+
```chemql
|
|
382
|
+
`cd ..`
|
|
383
|
+
`cd ~`
|
|
384
|
+
`dir`
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
The working directory persists between commands.
|
|
388
|
+
|
|
389
|
+
Environment variables and other shell state can also persist between commands, depending on the shell being used.
|
|
390
|
+
|
|
391
|
+
**Platform support**
|
|
392
|
+
|
|
393
|
+
- **Linux:** Bash
|
|
394
|
+
- **macOS:** Bash
|
|
395
|
+
- **Windows:** Command Prompt (`cmd.exe`)
|
|
396
|
+
|
|
397
|
+
PowerShell is not supported by the current implementation.
|
|
398
|
+
|
|
399
|
+
**How it works**
|
|
400
|
+
|
|
401
|
+
ChemQL maintains a persistent shell process and sends commands to it sequentially. Command output is captured and displayed in the ChemQL interface, allowing shell state to persist throughout the session.
|
|
402
|
+
|
|
403
|
+
**Notes**
|
|
404
|
+
|
|
405
|
+
- Commands must be enclosed in backticks to be interpreted as shell commands rather than ChemQL statements.
|
|
406
|
+
- Shell syntax differs between operating systems. Use commands appropriate for your platform.
|
|
407
|
+
- Commands run with the permissions of the process running ChemQL and may modify files or affect your system.
|
|
408
|
+
- Interactive terminal programs and commands requiring direct terminal input may not work correctly because the shell uses pipes rather than a real terminal.
|
|
409
|
+
|
|
410
|
+
## Data and external services
|
|
411
|
+
|
|
412
|
+
ChemQL loads bundled JSON datasets for elements, molecules, reactions,
|
|
413
|
+
molecule bonds, and bond energies. Some molecule lookups may query
|
|
414
|
+
PubChem or other external sources when the local dataset does not
|
|
415
|
+
contain a match.
|
|
416
|
+
|
|
417
|
+
The periodic-table dataset is based on PeriodicTableJSON by
|
|
418
|
+
Bowserinator. Review the dataset's accompanying license and attribution
|
|
419
|
+
requirements when redistributing it or derivative data. The licenses and
|
|
420
|
+
sources for the other bundled datasets should be checked in their
|
|
421
|
+
respective files before redistribution.
|
|
422
|
+
|
|
423
|
+
## Safety note
|
|
424
|
+
|
|
425
|
+
ChemQL's hybrid Python functionality can execute Python code. If your
|
|
426
|
+
installed version supports backtick-wrapped shell commands, those can
|
|
427
|
+
execute operating-system commands as well. Do not run untrusted ChemQL
|
|
428
|
+
scripts or queries.
|
|
429
|
+
|
|
430
|
+
## Development notes
|
|
431
|
+
|
|
432
|
+
- The interactive shell uses `prompt_toolkit`.
|
|
433
|
+
- The command-line interface supports single-command execution (`-c`)
|
|
434
|
+
and file execution (`-f`).
|
|
435
|
+
- Consult the package's source and included `help` output for the
|
|
436
|
+
exact commands and fields supported by your installed version.
|
|
437
|
+
|
|
438
|
+
## License
|
|
439
|
+
|
|
440
|
+
[MIT License](https://github.com/rakibulofficial1017/ChemQL/blob/main/LICENSE)
|
|
441
|
+
|
|
442
|
+
Created by [Mohammad Rakibul Islam](https://github.com/rakibulofficial1017).
|
|
443
|
+
Source code: [rakibulofficial1017/ChemQL](https://github.com/rakibulofficial1017/ChemQL).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
chemql/__init__.py,sha256=
|
|
1
|
+
chemql/__init__.py,sha256=QVKr8cWmQyaiLjGkWX0KijyOIj9qWMUYJJ9PJR-4F9A,109275
|
|
2
2
|
chemql/__main__.py,sha256=3Q52s0hyuouhvSSqFEQFDr6mtJ-azkCwI2AD4c7f7A8,28
|
|
3
3
|
chemql/data/BondEnergies.json,sha256=QZN3iWZrD8125jV41EG1GiRohBtP0CzAP_EDQTfaEKc,8269
|
|
4
4
|
chemql/data/MoleculeBonds.json,sha256=Iw2shYxJScH_tI80IytBYoqVDSbgT7OSqPJUIgKZme4,84262
|
|
@@ -8,9 +8,9 @@ chemql/data/Reactions.json,sha256=BFpkKEWwLzG54ot96BVOWwU3btpimRJlOM57yGclGgc,15
|
|
|
8
8
|
chemql/data/THIRD_PARTY_LICENSES/PeriodicTableJSON.txt,sha256=9wih5q8s58qR3vJEGpooa4ESyAGZHcmGvzwMMkMHNKk,322
|
|
9
9
|
chemql/data/THIRD_PARTY_LICENSES/RHEA.txt,sha256=nXTeCP6vJBW5B_mtaVGFLr0w7vnjrSTBnv6_eOk3KKo,634
|
|
10
10
|
chemql/html/periodic_table.html,sha256=ftVgXWjRO2AsTAGEekTrvgE5ZvcwRfoby9UWoixc9Y8,361560
|
|
11
|
-
chemql_python-0.
|
|
12
|
-
chemql_python-0.
|
|
13
|
-
chemql_python-0.
|
|
14
|
-
chemql_python-0.
|
|
15
|
-
chemql_python-0.
|
|
16
|
-
chemql_python-0.
|
|
11
|
+
chemql_python-1.0.1.dist-info/licenses/LICENSE,sha256=o4ywHC0EwRy2zgIASiRD2sK3Vcsf7LBcTHjDvoJgRE0,1079
|
|
12
|
+
chemql_python-1.0.1.dist-info/METADATA,sha256=wKVfuyvrj4AH0XDj-wgP_HySSfQeT447ZUvWLbvDQbo,11613
|
|
13
|
+
chemql_python-1.0.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
14
|
+
chemql_python-1.0.1.dist-info/entry_points.txt,sha256=j98ObVWR743an524z24AiWeRsppag-py5cc7l0D0HkI,39
|
|
15
|
+
chemql_python-1.0.1.dist-info/top_level.txt,sha256=9UzHwnk_F6W19Q9nayaFgVyAELQXXwoYjeBgB6MWNr8,7
|
|
16
|
+
chemql_python-1.0.1.dist-info/RECORD,,
|
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: chemql-python
|
|
3
|
-
Version: 0.2.2
|
|
4
|
-
Summary: A chemistry-focused query language and REPL
|
|
5
|
-
Requires-Python: >=3.10
|
|
6
|
-
Description-Content-Type: text/markdown
|
|
7
|
-
License-File: LICENSE
|
|
8
|
-
Requires-Dist: prompt_toolkit>=3.0
|
|
9
|
-
Provides-Extra: test
|
|
10
|
-
Requires-Dist: pytest>=8.0; extra == "test"
|
|
11
|
-
Dynamic: license-file
|
|
12
|
-
|
|
13
|
-
# ChemQL
|
|
14
|
-
|
|
15
|
-

|
|
16
|
-
|
|
17
|
-
ChemQL is a chemistry-focused query language and REPL for exploring element, molecule, and reaction datasets. It lets you search the built-in chemistry data, filter results with expressions, set reaction conditions, and combine queries with small Python snippets when needed.
|
|
18
|
-
|
|
19
|
-
## What ChemQL does
|
|
20
|
-
|
|
21
|
-
ChemQL supports:
|
|
22
|
-
|
|
23
|
-
- Searching the built-in `elements`, `molecules`, and `reactions` datasets
|
|
24
|
-
- Filtering by field, comparison operator, pattern matching, and boolean logic
|
|
25
|
-
- Quick lookup commands like `findel`, `findmol`, and `findre`
|
|
26
|
-
- Reaction setup with temperature, pressure, and catalyst configuration
|
|
27
|
-
- Running reactions with `react`
|
|
28
|
-
- Embedding Python in `[ ... ]` blocks and templates in `{{ ... }}`
|
|
29
|
-
- Running from a terminal REPL or executing a command/file from the shell
|
|
30
|
-
|
|
31
|
-
## Install the library
|
|
32
|
-
|
|
33
|
-
Install from this `chemql/` directory, which contains `pyproject.toml`:
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
cd /path/to/ChemQL/Language/chemql
|
|
37
|
-
python -m pip install -e ".[test]"
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
This installs the Python package and the `chemql` CLI:
|
|
41
|
-
|
|
42
|
-
```bash
|
|
43
|
-
chemql -c "search elements name = 'Hydrogen'"
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Start the interactive REPL with:
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
python -m chemql
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
You will see the interactive prompt:
|
|
53
|
-
|
|
54
|
-
```text
|
|
55
|
-
>>>
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
Type:
|
|
59
|
-
|
|
60
|
-
```text
|
|
61
|
-
help
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
or:
|
|
65
|
-
|
|
66
|
-
```text
|
|
67
|
-
?
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
to display the built-in help.
|
|
71
|
-
|
|
72
|
-
## Command-line usage
|
|
73
|
-
|
|
74
|
-
You can also execute a single ChemQL command or a file:
|
|
75
|
-
|
|
76
|
-
```bash
|
|
77
|
-
chemql -c 'search elements name = "Hydrogen"'
|
|
78
|
-
chemql -f examples/script.cql
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
The Python package is `chemql`. Its query engine, chemistry types, and bundled
|
|
82
|
-
datasets can be imported directly:
|
|
83
|
-
|
|
84
|
-
```python
|
|
85
|
-
from chemql import balance_stoichiometry, execute_query_text
|
|
86
|
-
|
|
87
|
-
result = execute_query_text('search elements name = "Hydrogen"')
|
|
88
|
-
reaction = balance_stoichiometry(["O2", "H2"], ["H2O"], reversible=True)
|
|
89
|
-
print(reaction)
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
## Core query syntax
|
|
93
|
-
|
|
94
|
-
### Search a dataset
|
|
95
|
-
|
|
96
|
-
```text
|
|
97
|
-
search elements
|
|
98
|
-
search molecules
|
|
99
|
-
search reactions
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### Search with filters
|
|
103
|
-
|
|
104
|
-
```text
|
|
105
|
-
search elements atomic_number > 10 and atomic_number < 20
|
|
106
|
-
search molecules name like "*water*"
|
|
107
|
-
search reactions reaction_type = "combustion"
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
### Sort, limit, and count
|
|
111
|
-
|
|
112
|
-
```text
|
|
113
|
-
search molecules sort molecular_weight desc
|
|
114
|
-
search molecules limit 10
|
|
115
|
-
search molecules count
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
### Return selected fields
|
|
119
|
-
|
|
120
|
-
```text
|
|
121
|
-
search molecules molecular_weight < 20 return name formula molecular_weight
|
|
122
|
-
search reactions reversible = true return name conditions
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
### Set the current source
|
|
126
|
-
|
|
127
|
-
```text
|
|
128
|
-
source elements
|
|
129
|
-
source molecules
|
|
130
|
-
source reactions
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
### List available keys
|
|
134
|
-
|
|
135
|
-
```text
|
|
136
|
-
list all
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
## Convenience lookup commands
|
|
140
|
-
|
|
141
|
-
```text
|
|
142
|
-
findel "He"
|
|
143
|
-
findel "Hydrogen"
|
|
144
|
-
|
|
145
|
-
findmol "H2O"
|
|
146
|
-
findmol "Water"
|
|
147
|
-
|
|
148
|
-
findre "Haber process"
|
|
149
|
-
findre "RHEA:10000" return name equation source
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
## Reactions
|
|
153
|
-
|
|
154
|
-
Set reaction conditions before running a reaction:
|
|
155
|
-
|
|
156
|
-
```text
|
|
157
|
-
set temperature 723K
|
|
158
|
-
set temperature standard
|
|
159
|
-
set pressure 20265000pa
|
|
160
|
-
set pressure room
|
|
161
|
-
set catalysts iron zinc
|
|
162
|
-
add catalyst "Vanadium oxide"
|
|
163
|
-
remove catalyst "Vanadium oxide"
|
|
164
|
-
conditions
|
|
165
|
-
react N2 3H2
|
|
166
|
-
balance O2 + H2 -> H2O
|
|
167
|
-
balance O2 + H2 <-> H2O
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
Temperature accepts `C`, `F`, or `K`; pressure accepts `Pa`, `N/m^2`, `Nm^-2`, `bar`, or `atm`. The keywords `standard` and `room` are also accepted.
|
|
171
|
-
|
|
172
|
-
`balance` accepts chemical formulas on each side, separated by `+`. It returns
|
|
173
|
-
a `Reaction` with the smallest positive integer stoichiometric coefficients.
|
|
174
|
-
The arrow determines reaction direction: `->` is irreversible and `<->` is
|
|
175
|
-
reversible. `//` begins a comment, as it does elsewhere in ChemQL. The function
|
|
176
|
-
API is `balance_stoichiometry(reactants, products, reversible=None)`.
|
|
177
|
-
|
|
178
|
-
## Operators
|
|
179
|
-
|
|
180
|
-
ChemQL supports these comparison operators:
|
|
181
|
-
|
|
182
|
-
- `=`
|
|
183
|
-
- `>`
|
|
184
|
-
- `<`
|
|
185
|
-
- `>=`
|
|
186
|
-
- `<=`
|
|
187
|
-
- `like`
|
|
188
|
-
- `like!`
|
|
189
|
-
- `has`
|
|
190
|
-
|
|
191
|
-
Boolean logic:
|
|
192
|
-
|
|
193
|
-
```text
|
|
194
|
-
and
|
|
195
|
-
or
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
Parentheses may be used to group expressions.
|
|
199
|
-
|
|
200
|
-
## Result controls
|
|
201
|
-
|
|
202
|
-
```text
|
|
203
|
-
search molecules first
|
|
204
|
-
search molecules last
|
|
205
|
-
search molecules sort atomic_number desc limit 5
|
|
206
|
-
search molecules count
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
## General commands
|
|
210
|
-
|
|
211
|
-
```text
|
|
212
|
-
list all
|
|
213
|
-
view
|
|
214
|
-
clear
|
|
215
|
-
dump my_session.txt
|
|
216
|
-
help
|
|
217
|
-
exit
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
`view` opens the periodic table in a browser. `dump` writes recent command history to a file.
|
|
221
|
-
|
|
222
|
-
## Hybrid Python mode
|
|
223
|
-
|
|
224
|
-
ChemQL can be mixed with Python inside square-bracket blocks:
|
|
225
|
-
|
|
226
|
-
```text
|
|
227
|
-
[x = 10]
|
|
228
|
-
[print(x)]
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
Conditional and loop blocks are also supported:
|
|
232
|
-
|
|
233
|
-
```text
|
|
234
|
-
[for x in range(3)]
|
|
235
|
-
[print(x)]
|
|
236
|
-
[endblock]
|
|
237
|
-
|
|
238
|
-
[if x > 0]
|
|
239
|
-
[print("positive")]
|
|
240
|
-
[else]
|
|
241
|
-
[print("not positive")]
|
|
242
|
-
[endblock]
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
Embedded ChemQL queries can also be used with double-brace syntax. This executes a query and uses the result as a value in the surrounding Python:
|
|
246
|
-
|
|
247
|
-
```text
|
|
248
|
-
[print({{ search elements number = 1 }})]
|
|
249
|
-
[for i in {{search elements number <= 10}}]
|
|
250
|
-
[print(i.name)]
|
|
251
|
-
[endblock]
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
The `{{ ... }}` form is a convenient way to make a query return actual data to Python, rather than only writing a plain query line.
|
|
255
|
-
|
|
256
|
-
## Line continuation
|
|
257
|
-
|
|
258
|
-
If a line ends with `\`, it continues onto the next line:
|
|
259
|
-
|
|
260
|
-
```text
|
|
261
|
-
search elements \
|
|
262
|
-
name = "Hydrogen"
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
This is treated as:
|
|
266
|
-
|
|
267
|
-
```text
|
|
268
|
-
search elements name = "Hydrogen"
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
## Examples
|
|
272
|
-
|
|
273
|
-
```text
|
|
274
|
-
search elements name = "Hydrogen"
|
|
275
|
-
findel "He"
|
|
276
|
-
findmol "H2O"
|
|
277
|
-
|
|
278
|
-
search molecules molecular_weight < 20 \
|
|
279
|
-
return name formula molecular_weight
|
|
280
|
-
|
|
281
|
-
search elements atomic_number > 10 \
|
|
282
|
-
and atomic_number < 20 \
|
|
283
|
-
sort atomic_number desc \
|
|
284
|
-
limit 5
|
|
285
|
-
|
|
286
|
-
search molecules count
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
## Repository layout
|
|
290
|
-
|
|
291
|
-
```text
|
|
292
|
-
chemql/
|
|
293
|
-
├── pyproject.toml
|
|
294
|
-
├── src/
|
|
295
|
-
│ └── chemql/
|
|
296
|
-
│ ├── __init__.py
|
|
297
|
-
│ ├── __main__.py
|
|
298
|
-
│ ├── data/
|
|
299
|
-
│ └── html/
|
|
300
|
-
├── tests/
|
|
301
|
-
├── scripts/
|
|
302
|
-
└── examples/
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
The installable package contains the language implementation and its bundled
|
|
306
|
-
chemical datasets.
|
|
307
|
-
|
|
308
|
-
Created by [Mohammad Rakibul Islam](https://github.com/rakibulofficial1017).
|
|
309
|
-
Source code: [rakibulofficial1017/ChemQL](https://github.com/rakibulofficial1017/ChemQL).
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|