chemql-python 0.2.2__py3-none-any.whl → 1.0.0__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 +285 -19
- chemql_python-1.0.0.dist-info/METADATA +373 -0
- {chemql_python-0.2.2.dist-info → chemql_python-1.0.0.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.0.dist-info}/WHEEL +0 -0
- {chemql_python-0.2.2.dist-info → chemql_python-1.0.0.dist-info}/entry_points.txt +0 -0
- {chemql_python-0.2.2.dist-info → chemql_python-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {chemql_python-0.2.2.dist-info → chemql_python-1.0.0.dist-info}/top_level.txt +0 -0
chemql/__init__.py
CHANGED
|
@@ -5,6 +5,8 @@ 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
|
|
@@ -28,6 +30,10 @@ __all__ = [
|
|
|
28
30
|
"balance_stoichiometry",
|
|
29
31
|
"execute_query",
|
|
30
32
|
"execute_query_text",
|
|
33
|
+
"ChemQLQuery",
|
|
34
|
+
"search",
|
|
35
|
+
"union",
|
|
36
|
+
"intersection",
|
|
31
37
|
]
|
|
32
38
|
|
|
33
39
|
|
|
@@ -112,7 +118,7 @@ with (DATA_DIR / "MoleculeBonds.json").open("r", encoding="utf-8") as f:
|
|
|
112
118
|
with (DATA_DIR / "BondEnergies.json").open("r", encoding="utf-8") as f:
|
|
113
119
|
bondEnergies = json.load(f)
|
|
114
120
|
|
|
115
|
-
operators = {"has", "like", "like!", "=", ">", "<", ">=", "<="}
|
|
121
|
+
operators = {"has", "like", "like!", "likep", "=", "!=", ">", "<", ">=", "<="}
|
|
116
122
|
|
|
117
123
|
queryOperations = {"sort", "limit", "count"}
|
|
118
124
|
|
|
@@ -1252,7 +1258,7 @@ def apply_query_operations(results, tokens, keys, positions=None):
|
|
|
1252
1258
|
return results
|
|
1253
1259
|
|
|
1254
1260
|
|
|
1255
|
-
functionMap = {"=": "__eq__", ">": "__gt__", "<": "__lt__", ">=": "__ge__", "<=": "__le__"}
|
|
1261
|
+
functionMap = {"=": "__eq__", "!=": "__ne__", ">": "__gt__", "<": "__lt__", ">=": "__ge__", "<=": "__le__"}
|
|
1256
1262
|
|
|
1257
1263
|
|
|
1258
1264
|
def convert_value(value, sample):
|
|
@@ -1296,14 +1302,35 @@ def parse_condition(tokens, index, source, keys, positions=None):
|
|
|
1296
1302
|
|
|
1297
1303
|
key = tokens[index]
|
|
1298
1304
|
operator = tokens[index + 1]
|
|
1299
|
-
|
|
1305
|
+
negated = operator.casefold() == "not"
|
|
1306
|
+
if negated:
|
|
1307
|
+
if index + 3 >= len(tokens):
|
|
1308
|
+
raise ChemQLError("Expected an operator and value after `not`", *(error_position(positions, index + 1) if positions else (None, 1))) # type: ignore
|
|
1309
|
+
operator = tokens[index + 2]
|
|
1310
|
+
value = tokens[index + 3]
|
|
1311
|
+
consumed = index + 4
|
|
1312
|
+
else:
|
|
1313
|
+
value = tokens[index + 2]
|
|
1314
|
+
consumed = index + 3
|
|
1300
1315
|
|
|
1301
1316
|
if key not in keys:
|
|
1302
1317
|
pos, length = error_position(positions, index) if positions else (None, 1)
|
|
1303
1318
|
raise ChemQLError(f"There is no key `{key}`", pos, length)
|
|
1304
1319
|
|
|
1305
|
-
if operator
|
|
1320
|
+
if operator in {"like", "like!", "likep"}:
|
|
1306
1321
|
pattern = value
|
|
1322
|
+
if operator == "likep":
|
|
1323
|
+
try:
|
|
1324
|
+
compiled_pattern = re.compile(pattern)
|
|
1325
|
+
except re.error as exc:
|
|
1326
|
+
pos, length = error_position(positions, index + 2) if positions else (None, 1)
|
|
1327
|
+
raise ChemQLError(f"Invalid regex: {exc}", pos, length) from exc
|
|
1328
|
+
|
|
1329
|
+
def regex_condition(item):
|
|
1330
|
+
actual = getattr(item, key)
|
|
1331
|
+
return isinstance(actual, str) and compiled_pattern.search(actual) is not None
|
|
1332
|
+
|
|
1333
|
+
return (lambda item, fn=regex_condition: not fn(item)) if negated else regex_condition, consumed
|
|
1307
1334
|
case_sensitive = operator == "like!"
|
|
1308
1335
|
|
|
1309
1336
|
def like_condition(item):
|
|
@@ -1318,7 +1345,7 @@ def parse_condition(tokens, index, source, keys, positions=None):
|
|
|
1318
1345
|
case_sensitive
|
|
1319
1346
|
)
|
|
1320
1347
|
|
|
1321
|
-
return
|
|
1348
|
+
return (lambda item, fn=like_condition: not fn(item)) if negated else like_condition, consumed
|
|
1322
1349
|
|
|
1323
1350
|
if operator == "has":
|
|
1324
1351
|
def has_condition(item):
|
|
@@ -1329,7 +1356,7 @@ def parse_condition(tokens, index, source, keys, positions=None):
|
|
|
1329
1356
|
except TypeError:
|
|
1330
1357
|
return False
|
|
1331
1358
|
|
|
1332
|
-
return
|
|
1359
|
+
return (lambda item, fn=has_condition: not fn(item)) if negated else has_condition, consumed
|
|
1333
1360
|
|
|
1334
1361
|
if operator not in functionMap:
|
|
1335
1362
|
pos, length = error_position(positions, index + 1) if positions else (None, 1)
|
|
@@ -1354,12 +1381,16 @@ def parse_condition(tokens, index, source, keys, positions=None):
|
|
|
1354
1381
|
except (TypeError, AttributeError):
|
|
1355
1382
|
return False
|
|
1356
1383
|
|
|
1357
|
-
return
|
|
1384
|
+
return (lambda item, fn=comparison_condition: not fn(item)) if negated else comparison_condition, consumed
|
|
1358
1385
|
|
|
1359
1386
|
def parse_primary(tokens, index, source, keys, positions=None):
|
|
1360
1387
|
if index >= len(tokens):
|
|
1361
1388
|
raise ChemQLError("Expected condition", *(error_position(positions, index) if positions else (None, 1)))#type:ignore
|
|
1362
1389
|
|
|
1390
|
+
if tokens[index].casefold() == "not":
|
|
1391
|
+
expression, next_index = parse_primary(tokens, index + 1, source, keys, positions)
|
|
1392
|
+
return (lambda item, fn=expression: not fn(item)), next_index
|
|
1393
|
+
|
|
1363
1394
|
if tokens[index] == "(":
|
|
1364
1395
|
expression, index = parse_or(
|
|
1365
1396
|
tokens,
|
|
@@ -1582,9 +1613,107 @@ def interpolate_python_queries(code):
|
|
|
1582
1613
|
python_globals.update(query_values)
|
|
1583
1614
|
return "".join(rewritten)
|
|
1584
1615
|
|
|
1616
|
+
def _space_query_operators(text):
|
|
1617
|
+
"""Separate comparison operators and parentheses outside quoted strings."""
|
|
1618
|
+
out, quote, escaped = [], None, False
|
|
1619
|
+
i = 0
|
|
1620
|
+
while i < len(text):
|
|
1621
|
+
ch = text[i]
|
|
1622
|
+
if quote:
|
|
1623
|
+
out.append(ch)
|
|
1624
|
+
if escaped:
|
|
1625
|
+
escaped = False
|
|
1626
|
+
elif ch == "\\\\":
|
|
1627
|
+
escaped = True
|
|
1628
|
+
elif ch == quote:
|
|
1629
|
+
quote = None
|
|
1630
|
+
i += 1
|
|
1631
|
+
continue
|
|
1632
|
+
if ch in ("'", '"'):
|
|
1633
|
+
quote = ch
|
|
1634
|
+
out.append(ch)
|
|
1635
|
+
i += 1
|
|
1636
|
+
continue
|
|
1637
|
+
if ch in "()":
|
|
1638
|
+
out.extend((" ", ch, " "))
|
|
1639
|
+
i += 1
|
|
1640
|
+
continue
|
|
1641
|
+
if ch in "<>!=" and i + 1 < len(text) and text[i + 1] == "=":
|
|
1642
|
+
out.extend((" ", ch + "=", " "))
|
|
1643
|
+
i += 2
|
|
1644
|
+
continue
|
|
1645
|
+
if ch in "<>!=":
|
|
1646
|
+
out.extend((" ", ch, " "))
|
|
1647
|
+
i += 1
|
|
1648
|
+
continue
|
|
1649
|
+
out.append(ch)
|
|
1650
|
+
i += 1
|
|
1651
|
+
return "".join(out)
|
|
1652
|
+
|
|
1653
|
+
|
|
1654
|
+
def _split_set_operations(text):
|
|
1655
|
+
"""Split top-level union/intersection outside quotes and parentheses, left-to-right."""
|
|
1656
|
+
parts, ops, start, depth = [], [], 0, 0
|
|
1657
|
+
quote, escaped = None, False
|
|
1658
|
+
words = re.finditer(r"\b(union|intersection)\b", text, re.IGNORECASE)
|
|
1659
|
+
for match in words:
|
|
1660
|
+
i = match.start()
|
|
1661
|
+
quote = None
|
|
1662
|
+
escaped = False
|
|
1663
|
+
depth = 0
|
|
1664
|
+
j = 0
|
|
1665
|
+
while j < i:
|
|
1666
|
+
ch = text[j]
|
|
1667
|
+
if quote:
|
|
1668
|
+
if escaped: escaped = False
|
|
1669
|
+
elif ch == "\\\\": escaped = True
|
|
1670
|
+
elif ch == quote: quote = None
|
|
1671
|
+
elif ch in ("'", '"'): quote = ch
|
|
1672
|
+
elif ch == "(": depth += 1
|
|
1673
|
+
elif ch == ")": depth -= 1
|
|
1674
|
+
j += 1
|
|
1675
|
+
if quote is None and depth == 0:
|
|
1676
|
+
parts.append(text[start:i].strip())
|
|
1677
|
+
ops.append(match.group(1).casefold())
|
|
1678
|
+
start = match.end()
|
|
1679
|
+
if not ops:
|
|
1680
|
+
return None
|
|
1681
|
+
parts.append(text[start:].strip())
|
|
1682
|
+
if any(not part for part in parts):
|
|
1683
|
+
raise ChemQLError("Missing query around union/intersection")
|
|
1684
|
+
return parts, ops
|
|
1685
|
+
|
|
1686
|
+
|
|
1687
|
+
def _as_result_list(result):
|
|
1688
|
+
if result is None or result == "No matching item":
|
|
1689
|
+
return []
|
|
1690
|
+
return result if isinstance(result, list) else [result]
|
|
1691
|
+
|
|
1692
|
+
|
|
1585
1693
|
def execute_query_text(text):
|
|
1586
1694
|
"""Execute plain query-language text."""
|
|
1587
|
-
text =
|
|
1695
|
+
text = text.strip()
|
|
1696
|
+
set_expression = _split_set_operations(text)
|
|
1697
|
+
if set_expression:
|
|
1698
|
+
parts, ops = set_expression
|
|
1699
|
+
result = _as_result_list(execute_query_text(parts[0]))
|
|
1700
|
+
for operator, part in zip(ops, parts[1:]):
|
|
1701
|
+
right = _as_result_list(execute_query_text(part))
|
|
1702
|
+
def identity(item):
|
|
1703
|
+
if hasattr(item, "id"): return ("id", getattr(item, "id"))
|
|
1704
|
+
if hasattr(item, "number"): return ("number", getattr(item, "number"))
|
|
1705
|
+
if hasattr(item, "cid") and getattr(item, "cid") is not None: return ("cid", getattr(item, "cid"))
|
|
1706
|
+
return (type(item).__name__, repr(item))
|
|
1707
|
+
right_map = {identity(item): item for item in right}
|
|
1708
|
+
if operator == "union":
|
|
1709
|
+
seen = {identity(item) for item in result}
|
|
1710
|
+
result.extend(item for item in right if identity(item) not in seen and not seen.add(identity(item)))
|
|
1711
|
+
else:
|
|
1712
|
+
result = [item for item in result if identity(item) in right_map]
|
|
1713
|
+
return result
|
|
1714
|
+
|
|
1715
|
+
text = interpolate_query(text)
|
|
1716
|
+
text = _space_query_operators(text)
|
|
1588
1717
|
positions = token_positions(text)
|
|
1589
1718
|
|
|
1590
1719
|
try:
|
|
@@ -1968,6 +2097,17 @@ def process_inline(text):
|
|
|
1968
2097
|
if not stripped[end + 2:].strip():
|
|
1969
2098
|
return execute_query_block(stripped[2:end].strip())
|
|
1970
2099
|
|
|
2100
|
+
# Backtick-wrapped commands execute through subprocess (not deprecated os.system).
|
|
2101
|
+
if len(stripped) >= 2 and stripped.startswith("`") and stripped.endswith("`"):
|
|
2102
|
+
command = stripped[1:-1]
|
|
2103
|
+
completed = subprocess.run(command, shell=True, text=True, capture_output=True)
|
|
2104
|
+
output = completed.stdout
|
|
2105
|
+
if completed.stderr:
|
|
2106
|
+
output += completed.stderr
|
|
2107
|
+
if completed.returncode and not output:
|
|
2108
|
+
output = f"Command exited with status {completed.returncode}"
|
|
2109
|
+
return output.rstrip("\\n")
|
|
2110
|
+
|
|
1971
2111
|
# Everything else is query language.
|
|
1972
2112
|
return execute_query_text(stripped)
|
|
1973
2113
|
|
|
@@ -2099,12 +2239,13 @@ def parse_temperature_setting(value):
|
|
|
2099
2239
|
if normalized == "room":
|
|
2100
2240
|
return 298.15
|
|
2101
2241
|
|
|
2102
|
-
match = re.fullmatch(r"([+-]?(?:\d+(?:\.\d*)?|\.\d+))\s
|
|
2242
|
+
match = re.fullmatch(r"([+-]?(?:\d+(?:\.\d*)?|\.\d+))\s*(?:°\s*)?(celsius|fahrenheit|kelvin|degc|degf|degk|c|f|k)", normalized)
|
|
2103
2243
|
if not match:
|
|
2104
2244
|
raise ValueError("Temperature must use C, F, or K, or be `standard` or `room`")
|
|
2105
2245
|
|
|
2106
2246
|
temperature = float(match.group(1))
|
|
2107
2247
|
unit = match.group(2)
|
|
2248
|
+
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
2249
|
if unit == "f":
|
|
2109
2250
|
temperature = (temperature - 32) * 5 / 9 + 273.15
|
|
2110
2251
|
elif unit == "c":
|
|
@@ -2130,14 +2271,16 @@ def parse_pressure_setting(value):
|
|
|
2130
2271
|
pressure = float(match.group(1))
|
|
2131
2272
|
unit = re.sub(r"\s+", "", match.group(2)).replace("²", "^2")
|
|
2132
2273
|
unit_scales = {
|
|
2133
|
-
"pa": 1.0,
|
|
2134
|
-
"
|
|
2135
|
-
"
|
|
2136
|
-
"
|
|
2137
|
-
"
|
|
2274
|
+
"pa": 1.0, "pascal": 1.0, "pascals": 1.0,
|
|
2275
|
+
"kpa": 1_000.0, "mpa": 1_000_000.0,
|
|
2276
|
+
"hpa": 100.0, "mbar": 100.0,
|
|
2277
|
+
"n/m^2": 1.0, "n/m2": 1.0, "nm^-2": 1.0, "n·m^-2": 1.0,
|
|
2278
|
+
"bar": 100_000.0, "mbar": 100.0,
|
|
2279
|
+
"atm": 101_325.0, "torr": 101_325.0 / 760, "mmhg": 101_325.0 / 760,
|
|
2280
|
+
"psi": 6894.757293168, "kpsi": 6_894_757.293168,
|
|
2138
2281
|
}
|
|
2139
2282
|
if unit not in unit_scales:
|
|
2140
|
-
raise ValueError("
|
|
2283
|
+
raise ValueError("Unsupported pressure unit. Try Pa, kPa, MPa, hPa, bar, atm, torr, mmHg, or psi")
|
|
2141
2284
|
|
|
2142
2285
|
return pressure * unit_scales[unit]
|
|
2143
2286
|
|
|
@@ -2550,6 +2693,8 @@ def execute(arguments, query_text=None, positions=None):
|
|
|
2550
2693
|
)
|
|
2551
2694
|
|
|
2552
2695
|
source, keys = SOURCES[source_name]
|
|
2696
|
+
if i >= len(arguments):
|
|
2697
|
+
return list(source)
|
|
2553
2698
|
return_attributes = None
|
|
2554
2699
|
|
|
2555
2700
|
if "return" in arguments[i:]:
|
|
@@ -2941,6 +3086,18 @@ class ChemqlLexer(Lexer):
|
|
|
2941
3086
|
|
|
2942
3087
|
i = end + 2
|
|
2943
3088
|
continue
|
|
3089
|
+
|
|
3090
|
+
if line.startswith("//", i):
|
|
3091
|
+
|
|
3092
|
+
end = len(line)
|
|
3093
|
+
|
|
3094
|
+
tokens.append((
|
|
3095
|
+
"class:comment",
|
|
3096
|
+
line[i:end]
|
|
3097
|
+
))
|
|
3098
|
+
|
|
3099
|
+
i = end
|
|
3100
|
+
continue
|
|
2944
3101
|
|
|
2945
3102
|
if line[i] == "[":
|
|
2946
3103
|
end = line.find("]", i + 1)
|
|
@@ -2955,6 +3112,20 @@ class ChemqlLexer(Lexer):
|
|
|
2955
3112
|
|
|
2956
3113
|
i = end + 1
|
|
2957
3114
|
continue
|
|
3115
|
+
|
|
3116
|
+
if line[i] == "`":
|
|
3117
|
+
end = line.find("`", i + 1)
|
|
3118
|
+
|
|
3119
|
+
if end == -1:
|
|
3120
|
+
end = len(line) - 1
|
|
3121
|
+
|
|
3122
|
+
tokens.append((
|
|
3123
|
+
"class:terminalcommand",
|
|
3124
|
+
line[i:end + 1]
|
|
3125
|
+
))
|
|
3126
|
+
|
|
3127
|
+
i = end + 1
|
|
3128
|
+
continue
|
|
2958
3129
|
|
|
2959
3130
|
# Strings
|
|
2960
3131
|
if line[i] in "\"'":
|
|
@@ -2983,7 +3154,7 @@ class ChemqlLexer(Lexer):
|
|
|
2983
3154
|
# Operators
|
|
2984
3155
|
matched_operator = None
|
|
2985
3156
|
|
|
2986
|
-
for operator in (">=", "<=", "=", ">", "<"):
|
|
3157
|
+
for operator in ("!=", ">=", "<=", "=", ">", "<"):
|
|
2987
3158
|
if line.startswith(operator, i):
|
|
2988
3159
|
matched_operator = operator
|
|
2989
3160
|
break
|
|
@@ -3040,7 +3211,9 @@ class ChemqlLexer(Lexer):
|
|
|
3040
3211
|
"conditions",
|
|
3041
3212
|
"view",
|
|
3042
3213
|
"help",
|
|
3043
|
-
"dump"
|
|
3214
|
+
"dump",
|
|
3215
|
+
"union",
|
|
3216
|
+
"intersection",
|
|
3044
3217
|
}:
|
|
3045
3218
|
style = "class:command"
|
|
3046
3219
|
|
|
@@ -3058,6 +3231,7 @@ class ChemqlLexer(Lexer):
|
|
|
3058
3231
|
"has",
|
|
3059
3232
|
"like",
|
|
3060
3233
|
"like!",
|
|
3234
|
+
"likep",
|
|
3061
3235
|
"sort",
|
|
3062
3236
|
"limit",
|
|
3063
3237
|
"count",
|
|
@@ -3152,7 +3326,7 @@ class ChemqlCompleter(Completer):
|
|
|
3152
3326
|
if source_name in SOURCES:
|
|
3153
3327
|
_, keys = SOURCES[source_name]
|
|
3154
3328
|
suggestions = list(keys) + [
|
|
3155
|
-
"=", ">", "<", ">=", "<=", "like", "like!", "has",
|
|
3329
|
+
"=", ">", "<", ">=", "<=", "!=", "like", "like!", "likep", "has",
|
|
3156
3330
|
"sort", "limit", "count", "asc", "desc", "return",
|
|
3157
3331
|
]
|
|
3158
3332
|
|
|
@@ -3167,6 +3341,8 @@ class ChemqlCompleter(Completer):
|
|
|
3167
3341
|
|
|
3168
3342
|
chemql_style = Style.from_dict({
|
|
3169
3343
|
"command": "ansicyan",
|
|
3344
|
+
"terminalcommand": "ansipurple",
|
|
3345
|
+
"comment": "#545454 bold underline",
|
|
3170
3346
|
"source": "ansiblue",
|
|
3171
3347
|
"key": "ansimagenta",
|
|
3172
3348
|
"operator": "ansiyellow",
|
|
@@ -3178,6 +3354,90 @@ chemql_style = Style.from_dict({
|
|
|
3178
3354
|
})
|
|
3179
3355
|
|
|
3180
3356
|
|
|
3357
|
+
|
|
3358
|
+
|
|
3359
|
+
class _ChemQLField:
|
|
3360
|
+
"""Field selector used by the fluent Python query API."""
|
|
3361
|
+
def __init__(self, query, field):
|
|
3362
|
+
self.query = query
|
|
3363
|
+
self.field = field
|
|
3364
|
+
|
|
3365
|
+
def condition(self, **conditions):
|
|
3366
|
+
operators = {
|
|
3367
|
+
"eq": "=", "ne": "!=", "lt": "<", "lte": "<=",
|
|
3368
|
+
"gt": ">", "gte": ">=", "like": "like", "likep": "likep",
|
|
3369
|
+
"has": "has",
|
|
3370
|
+
}
|
|
3371
|
+
if len(conditions) != 1:
|
|
3372
|
+
raise ValueError("condition() expects exactly one condition, e.g. lt=10")
|
|
3373
|
+
name, value = next(iter(conditions.items()))
|
|
3374
|
+
if name not in operators:
|
|
3375
|
+
raise ValueError(f"Unknown condition {name!r}; use {', '.join(operators)}")
|
|
3376
|
+
rendered = shlex.quote(str(value))
|
|
3377
|
+
return ChemQLQuery(f"{self.query.query} {self.field} {operators[name]} {rendered}")
|
|
3378
|
+
|
|
3379
|
+
|
|
3380
|
+
class ChemQLQuery:
|
|
3381
|
+
"""Composable ChemQL query. Use .execute() to evaluate it."""
|
|
3382
|
+
def __init__(self, query):
|
|
3383
|
+
self.query = str(query).strip()
|
|
3384
|
+
|
|
3385
|
+
def __getattr__(self, field):
|
|
3386
|
+
if field.startswith("_"):
|
|
3387
|
+
raise AttributeError(field)
|
|
3388
|
+
return _ChemQLField(self, field)
|
|
3389
|
+
|
|
3390
|
+
def return_(self, *attributes):
|
|
3391
|
+
if not attributes:
|
|
3392
|
+
raise ValueError("return_() requires at least one attribute")
|
|
3393
|
+
return ChemQLQuery(f"{self.query} return " + " ".join(attributes))
|
|
3394
|
+
|
|
3395
|
+
def execute(self):
|
|
3396
|
+
return execute_query_text(self.query)
|
|
3397
|
+
|
|
3398
|
+
def __or__(self, other):
|
|
3399
|
+
if not isinstance(other, ChemQLQuery):
|
|
3400
|
+
return NotImplemented
|
|
3401
|
+
return ChemQLQuery(f"{self.query} union {other.query}")
|
|
3402
|
+
|
|
3403
|
+
def __and__(self, other):
|
|
3404
|
+
if not isinstance(other, ChemQLQuery):
|
|
3405
|
+
return NotImplemented
|
|
3406
|
+
return ChemQLQuery(f"{self.query} intersection {other.query}")
|
|
3407
|
+
|
|
3408
|
+
def __str__(self):
|
|
3409
|
+
return f"Query of `{self.query}`"
|
|
3410
|
+
|
|
3411
|
+
def __repr__(self):
|
|
3412
|
+
return str(self)
|
|
3413
|
+
|
|
3414
|
+
|
|
3415
|
+
def search(source):
|
|
3416
|
+
"""Create a native Python API query, e.g. search('elements')."""
|
|
3417
|
+
source = str(source).strip()
|
|
3418
|
+
if source not in SOURCES:
|
|
3419
|
+
raise ValueError(f"Unknown ChemQL source {source!r}; expected one of {', '.join(SOURCES)}")
|
|
3420
|
+
return ChemQLQuery(f"search {source}")
|
|
3421
|
+
|
|
3422
|
+
|
|
3423
|
+
def union(x, y):
|
|
3424
|
+
"""Return the left-to-right union of two ChemQLQuery objects."""
|
|
3425
|
+
if not isinstance(x, ChemQLQuery) or not isinstance(y, ChemQLQuery):
|
|
3426
|
+
raise TypeError("union() expects two ChemQLQuery objects")
|
|
3427
|
+
return x | y
|
|
3428
|
+
|
|
3429
|
+
|
|
3430
|
+
def intersection(x, y):
|
|
3431
|
+
"""Return the intersection of two ChemQLQuery objects."""
|
|
3432
|
+
if not isinstance(x, ChemQLQuery) or not isinstance(y, ChemQLQuery):
|
|
3433
|
+
raise TypeError("intersection() expects two ChemQLQuery objects")
|
|
3434
|
+
return x & y
|
|
3435
|
+
|
|
3436
|
+
|
|
3437
|
+
ChemQLQuery.__name__ = "ChemQLQuery"
|
|
3438
|
+
ChemQLQuery.__qualname__ = "ChemQLQuery"
|
|
3439
|
+
|
|
3440
|
+
|
|
3181
3441
|
def main():
|
|
3182
3442
|
parser = argparse.ArgumentParser(
|
|
3183
3443
|
description="Chemql query and chemistry language"
|
|
@@ -3199,7 +3459,7 @@ def main():
|
|
|
3199
3459
|
"-v",
|
|
3200
3460
|
"--version",
|
|
3201
3461
|
action="version",
|
|
3202
|
-
version="0.
|
|
3462
|
+
version="1.0.0",
|
|
3203
3463
|
)
|
|
3204
3464
|
|
|
3205
3465
|
args = parser.parse_args()
|
|
@@ -3212,6 +3472,12 @@ def main():
|
|
|
3212
3472
|
code = f.read()
|
|
3213
3473
|
process_lines(code.splitlines())
|
|
3214
3474
|
|
|
3475
|
+
elif not sys.stdin.isatty():
|
|
3476
|
+
piped = sys.stdin.read()
|
|
3477
|
+
if piped.strip():
|
|
3478
|
+
process_lines([piped.strip()])
|
|
3479
|
+
else:
|
|
3480
|
+
run_interactive()
|
|
3215
3481
|
else:
|
|
3216
3482
|
run_interactive()
|
|
3217
3483
|
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chemql-python
|
|
3
|
+
Version: 1.0.0
|
|
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
|
+
## Quick start
|
|
52
|
+
|
|
53
|
+
Start the interactive terminal:
|
|
54
|
+
|
|
55
|
+
``` bash
|
|
56
|
+
chemql
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Then try:
|
|
60
|
+
|
|
61
|
+
``` text
|
|
62
|
+
search elements name = "Hydrogen"
|
|
63
|
+
findel "He"
|
|
64
|
+
findmol "H2O"
|
|
65
|
+
search molecules molecular_weight < 20 return name formula molecular_weight
|
|
66
|
+
search reactions reaction_type = "combustion" return name equation
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Show the built-in command reference from the REPL:
|
|
70
|
+
|
|
71
|
+
``` text
|
|
72
|
+
help
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Command-line usage
|
|
76
|
+
|
|
77
|
+
Run a single command with `-c`:
|
|
78
|
+
|
|
79
|
+
``` bash
|
|
80
|
+
chemql -c 'search elements name = "Hydrogen"'
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Run commands from a file with `-f`:
|
|
84
|
+
|
|
85
|
+
``` bash
|
|
86
|
+
chemql -f queries.chemql
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Check the installed version:
|
|
90
|
+
|
|
91
|
+
``` bash
|
|
92
|
+
chemql --version
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
A query file can contain multiple ChemQL commands, one per line. Use the
|
|
96
|
+
`//` comment marker for comments and a trailing backslash (`\`) to
|
|
97
|
+
continue a command onto the next line.
|
|
98
|
+
|
|
99
|
+
Example `queries.chemql`:
|
|
100
|
+
|
|
101
|
+
``` text
|
|
102
|
+
// Find lighter molecules
|
|
103
|
+
search molecules molecular_weight < 20 return name formula molecular_weight
|
|
104
|
+
|
|
105
|
+
// Show a subset of the periodic table
|
|
106
|
+
search elements atomic_number > 10 and atomic_number < 20 sort atomic_number desc limit 5
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Query language
|
|
110
|
+
|
|
111
|
+
### Data sources
|
|
112
|
+
|
|
113
|
+
ChemQL provides three main sources:
|
|
114
|
+
|
|
115
|
+
Source Contents
|
|
116
|
+
------------- ----------------------------------------------
|
|
117
|
+
`elements` Periodic-table element data
|
|
118
|
+
`molecules` Molecule properties and identifiers
|
|
119
|
+
`reactions` Reaction equations, conditions, and metadata
|
|
120
|
+
|
|
121
|
+
Search a source explicitly:
|
|
122
|
+
|
|
123
|
+
``` text
|
|
124
|
+
search elements
|
|
125
|
+
search molecules
|
|
126
|
+
search reactions
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
You can also use `search all` before a source name, for example:
|
|
130
|
+
|
|
131
|
+
``` text
|
|
132
|
+
search all elements atomic_mass < 100
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Change the active source in an interactive session with:
|
|
136
|
+
|
|
137
|
+
``` text
|
|
138
|
+
source elements
|
|
139
|
+
source molecules
|
|
140
|
+
source reactions
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Filtering
|
|
144
|
+
|
|
145
|
+
Use comparison operators to filter records:
|
|
146
|
+
|
|
147
|
+
Operator Meaning
|
|
148
|
+
---------- ----------------------------------------
|
|
149
|
+
`=` Equal
|
|
150
|
+
`!=` Not equal
|
|
151
|
+
`>` Greater than
|
|
152
|
+
`<` Less than
|
|
153
|
+
`>=` Greater than or equal
|
|
154
|
+
`<=` Less than or equal
|
|
155
|
+
`like` Case-insensitive wildcard match
|
|
156
|
+
`like!` Case-sensitive wildcard match
|
|
157
|
+
`likep` Python regular-expression match
|
|
158
|
+
`has` Check whether a value contains an item
|
|
159
|
+
|
|
160
|
+
Examples:
|
|
161
|
+
|
|
162
|
+
``` text
|
|
163
|
+
search elements atomic_number > 10
|
|
164
|
+
search molecules molecular_weight <= 20
|
|
165
|
+
search molecules name like "*water*"
|
|
166
|
+
search reactions id likep ".*haber(\s*|-|_)process.*"
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The wildcard matcher supports `*` for a sequence of characters and other
|
|
170
|
+
wildcard tokens supported by ChemQL's pattern matcher. Use `likep` when
|
|
171
|
+
you need regular expressions rather than wildcard patterns.
|
|
172
|
+
|
|
173
|
+
### Logical conditions and parentheses
|
|
174
|
+
|
|
175
|
+
Combine conditions with `and`, `or`, and `not`. Parentheses group
|
|
176
|
+
conditions. Comparison operators and parentheses can be written without
|
|
177
|
+
surrounding spaces.
|
|
178
|
+
|
|
179
|
+
``` text
|
|
180
|
+
search elements name not like "*ium"
|
|
181
|
+
search elements name not like "*ium" and not atomic_mass < 50
|
|
182
|
+
search elements atomic_mass > 50 and (density<10 or boil<100)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Combining query results
|
|
186
|
+
|
|
187
|
+
Use `union` to combine results and `intersection` to keep records
|
|
188
|
+
present in both result lists. Operations are evaluated from left to
|
|
189
|
+
right.
|
|
190
|
+
|
|
191
|
+
``` text
|
|
192
|
+
search elements name not like "*ium" intersection search elements name like "*um"
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Use the Python API's query-object operators `|` and `&` for union and
|
|
196
|
+
intersection, respectively, when working with `ChemQLQuery` objects.
|
|
197
|
+
|
|
198
|
+
### Sorting, limiting, counting, and returning fields
|
|
199
|
+
|
|
200
|
+
``` text
|
|
201
|
+
search elements sort atomic_number
|
|
202
|
+
search molecules sort molecular_weight desc
|
|
203
|
+
search molecules limit 10
|
|
204
|
+
search molecules count
|
|
205
|
+
search molecules molecular_weight < 20 return name
|
|
206
|
+
search molecules molecular_weight < 20 return name formula molecular_weight
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
- `sort <field> [asc|desc]` sorts results.
|
|
210
|
+
- `limit <number>` limits the number of results.
|
|
211
|
+
- `count` returns the number of matches.
|
|
212
|
+
- `return <field...>` returns only selected fields. One field produces
|
|
213
|
+
a list; multiple fields produce a table-like result.
|
|
214
|
+
- `first` and `last` can be used to select one result.
|
|
215
|
+
|
|
216
|
+
### Find commands
|
|
217
|
+
|
|
218
|
+
``` text
|
|
219
|
+
findel "He"
|
|
220
|
+
findel "Hydrogen"
|
|
221
|
+
|
|
222
|
+
findmol "H2O"
|
|
223
|
+
findmol "Water"
|
|
224
|
+
|
|
225
|
+
findre "Haber process"
|
|
226
|
+
findre "RHEA:10000" return name equation source
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`findmol` searches the local molecule dataset first and can use external
|
|
230
|
+
chemical-data services when a local match is not found. External lookups
|
|
231
|
+
require an internet connection and may be unavailable if the service is
|
|
232
|
+
down.
|
|
233
|
+
|
|
234
|
+
## Reactions and equation balancing
|
|
235
|
+
|
|
236
|
+
ChemQL includes reaction data and commands for working with reaction
|
|
237
|
+
conditions.
|
|
238
|
+
|
|
239
|
+
``` text
|
|
240
|
+
findre "Haber process"
|
|
241
|
+
balance O2 + H2 -> H2O
|
|
242
|
+
balance O2 + H2 <-> H2O
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
The `->` arrow denotes an irreversible reaction, while `<->` denotes a
|
|
246
|
+
reversible reaction.
|
|
247
|
+
|
|
248
|
+
Set conditions before using `react`:
|
|
249
|
+
|
|
250
|
+
``` text
|
|
251
|
+
set temperature 723K
|
|
252
|
+
set pressure 20265000pa
|
|
253
|
+
set catalysts iron zinc
|
|
254
|
+
conditions
|
|
255
|
+
react N2 3H2
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Temperature can be supplied in Celsius (`C`), Fahrenheit (`F`), or
|
|
259
|
+
Kelvin (`K`). Pressure supports units such as pascals and bar; the
|
|
260
|
+
available unit conversions depend on the installed version. Preset
|
|
261
|
+
conditions are also available:
|
|
262
|
+
|
|
263
|
+
``` text
|
|
264
|
+
set temperature standard
|
|
265
|
+
set pressure room
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Use `help` in the terminal for the command reference included with your
|
|
269
|
+
installed version.
|
|
270
|
+
|
|
271
|
+
## Python API
|
|
272
|
+
|
|
273
|
+
Import ChemQL from Python:
|
|
274
|
+
|
|
275
|
+
``` python
|
|
276
|
+
import chemql
|
|
277
|
+
|
|
278
|
+
result = chemql.execute_query_text(
|
|
279
|
+
'search elements name = "Hydrogen"'
|
|
280
|
+
)
|
|
281
|
+
print(result)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
You can also use `execute_query` for a query string:
|
|
285
|
+
|
|
286
|
+
``` python
|
|
287
|
+
import chemql
|
|
288
|
+
|
|
289
|
+
result = chemql.execute_query('search molecules molecular_weight < 20')
|
|
290
|
+
print(result)
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
The package exposes chemistry data classes and helpers, including
|
|
294
|
+
`Element`, `Molecule`, `Reaction`, `ChemQLError`, `Unknown`,
|
|
295
|
+
`ReturnTable`, and `balance_stoichiometry`.
|
|
296
|
+
|
|
297
|
+
### Query builder API
|
|
298
|
+
|
|
299
|
+
Versions that include the query-builder API also support chaining query
|
|
300
|
+
conditions:
|
|
301
|
+
|
|
302
|
+
``` python
|
|
303
|
+
import chemql
|
|
304
|
+
|
|
305
|
+
x = chemql.search("elements")
|
|
306
|
+
y = x.atomic_mass.condition(lt=10)
|
|
307
|
+
z = y.return_("name")
|
|
308
|
+
|
|
309
|
+
print(z) # The query as text
|
|
310
|
+
print(z.execute()) # Execute the query
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Query objects can be combined with `|` (union) and `&` (intersection).
|
|
314
|
+
The convenience functions `chemql.union(x, y)` and
|
|
315
|
+
`chemql.intersection(x, y)` are also available in versions that include
|
|
316
|
+
this API.
|
|
317
|
+
|
|
318
|
+
## Hybrid Python
|
|
319
|
+
|
|
320
|
+
ChemQL can execute Python expressions inside square brackets:
|
|
321
|
+
|
|
322
|
+
``` text
|
|
323
|
+
[x = 10]
|
|
324
|
+
[print(x)]
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
It also supports Python control blocks using `[for ...]`, `[if ...]`,
|
|
328
|
+
`[elif ...]`, `[else]`, `[while ...]`, `[break]`, `[continue]`, and
|
|
329
|
+
`[endblock]`.
|
|
330
|
+
|
|
331
|
+
Use `{{ ... }}` to evaluate a ChemQL query and insert its result into
|
|
332
|
+
Python:
|
|
333
|
+
|
|
334
|
+
``` text
|
|
335
|
+
[print({{ search elements atomic_number = 1 }})]
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Only run Python code and shell commands from sources you trust.
|
|
339
|
+
|
|
340
|
+
## Data and external services
|
|
341
|
+
|
|
342
|
+
ChemQL loads bundled JSON datasets for elements, molecules, reactions,
|
|
343
|
+
molecule bonds, and bond energies. Some molecule lookups may query
|
|
344
|
+
PubChem or other external sources when the local dataset does not
|
|
345
|
+
contain a match.
|
|
346
|
+
|
|
347
|
+
The periodic-table dataset is based on PeriodicTableJSON by
|
|
348
|
+
Bowserinator. Review the dataset's accompanying license and attribution
|
|
349
|
+
requirements when redistributing it or derivative data. The licenses and
|
|
350
|
+
sources for the other bundled datasets should be checked in their
|
|
351
|
+
respective files before redistribution.
|
|
352
|
+
|
|
353
|
+
## Safety note
|
|
354
|
+
|
|
355
|
+
ChemQL's hybrid Python functionality can execute Python code. If your
|
|
356
|
+
installed version supports backtick-wrapped shell commands, those can
|
|
357
|
+
execute operating-system commands as well. Do not run untrusted ChemQL
|
|
358
|
+
scripts or queries.
|
|
359
|
+
|
|
360
|
+
## Development notes
|
|
361
|
+
|
|
362
|
+
- The interactive shell uses `prompt_toolkit`.
|
|
363
|
+
- The command-line interface supports single-command execution (`-c`)
|
|
364
|
+
and file execution (`-f`).
|
|
365
|
+
- Consult the package's source and included `help` output for the
|
|
366
|
+
exact commands and fields supported by your installed version.
|
|
367
|
+
|
|
368
|
+
## License
|
|
369
|
+
|
|
370
|
+
[MIT License](https://github.com/rakibulofficial1017/ChemQL/blob/main/LICENSE)
|
|
371
|
+
|
|
372
|
+
Created by [Mohammad Rakibul Islam](https://github.com/rakibulofficial1017).
|
|
373
|
+
Source code: [rakibulofficial1017/ChemQL](https://github.com/rakibulofficial1017/ChemQL).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
chemql/__init__.py,sha256=
|
|
1
|
+
chemql/__init__.py,sha256=1lbJuAYddBEUEhTXDmgIVPVo7B616pCO-MPmf68jgvg,105921
|
|
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.0.dist-info/licenses/LICENSE,sha256=o4ywHC0EwRy2zgIASiRD2sK3Vcsf7LBcTHjDvoJgRE0,1079
|
|
12
|
+
chemql_python-1.0.0.dist-info/METADATA,sha256=mtxMpsFDnUNZ7EHKdXrG83PJevsh2FgJpg8fpWkLKAM,9747
|
|
13
|
+
chemql_python-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
14
|
+
chemql_python-1.0.0.dist-info/entry_points.txt,sha256=j98ObVWR743an524z24AiWeRsppag-py5cc7l0D0HkI,39
|
|
15
|
+
chemql_python-1.0.0.dist-info/top_level.txt,sha256=9UzHwnk_F6W19Q9nayaFgVyAELQXXwoYjeBgB6MWNr8,7
|
|
16
|
+
chemql_python-1.0.0.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
|