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 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
- value = tokens[index + 2]
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 == "like" or operator == "like!":
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 like_condition, index + 3
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 has_condition, index + 3
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 comparison_condition, index + 3
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 = interpolate_query(text.strip())
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*°?\s*([cfk])", normalized)
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
- "n/m^2": 1.0,
2135
- "nm^-2": 1.0,
2136
- "bar": 100000.0,
2137
- "atm": 101325.0,
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("Pressure must use Pa, N/m^2, Nm^-2, bar, or atm")
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.2.2",
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=c0qkGIxP9o0z45qnlzXrwx-e_7Yms4M_W7q5aaqPAzA,96023
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.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.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
- ![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).