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 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
- value = tokens[index + 2]
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 == "like" or operator == "like!":
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 like_condition, index + 3
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 has_condition, index + 3
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 comparison_condition, index + 3
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 = interpolate_query(text.strip())
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*°?\s*([cfk])", normalized)
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
- "n/m^2": 1.0,
2135
- "nm^-2": 1.0,
2136
- "bar": 100000.0,
2137
- "atm": 101325.0,
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("Pressure must use Pa, N/m^2, Nm^-2, bar, or atm")
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.2.2",
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.command:
3208
- process_lines(args.command.splitlines())
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=c0qkGIxP9o0z45qnlzXrwx-e_7Yms4M_W7q5aaqPAzA,96023
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.2.2.dist-info/licenses/LICENSE,sha256=o4ywHC0EwRy2zgIASiRD2sK3Vcsf7LBcTHjDvoJgRE0,1079
12
- chemql_python-0.2.2.dist-info/METADATA,sha256=QSmDmVczOBEgJVBUeZdqhE_pz3YlMIIDXMWm4-hDnl4,6177
13
- chemql_python-0.2.2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
14
- chemql_python-0.2.2.dist-info/entry_points.txt,sha256=j98ObVWR743an524z24AiWeRsppag-py5cc7l0D0HkI,39
15
- chemql_python-0.2.2.dist-info/top_level.txt,sha256=9UzHwnk_F6W19Q9nayaFgVyAELQXXwoYjeBgB6MWNr8,7
16
- chemql_python-0.2.2.dist-info/RECORD,,
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
- ![ChemQL logo](logo.png)
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).