chemql-python 1.0.0__py3-none-any.whl → 1.0.2__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
@@ -1,6 +1,7 @@
1
1
  import json
2
2
  import math
3
3
  import shlex
4
+ import os
4
5
  import re
5
6
  import ast
6
7
  import argparse
@@ -10,6 +11,8 @@ import sys
10
11
  from pathlib import Path
11
12
  from prompt_toolkit import PromptSession
12
13
  from prompt_toolkit.lexers import Lexer
14
+ import atexit
15
+ import uuid
13
16
  from prompt_toolkit.styles import Style
14
17
  from prompt_toolkit.completion import Completer, Completion
15
18
  import urllib.parse
@@ -34,8 +37,95 @@ __all__ = [
34
37
  "search",
35
38
  "union",
36
39
  "intersection",
40
+ "__version__"
37
41
  ]
38
42
 
43
+ __version__ = "1.0.2"
44
+
45
+
46
+ _shell_process = None
47
+
48
+
49
+ def get_shell():
50
+ global _shell_process
51
+
52
+ if _shell_process is None or _shell_process.poll() is not None:
53
+ if os.name == "nt":
54
+ _shell_process = subprocess.Popen(
55
+ ["cmd.exe", "/D", "/Q", "/K", "prompt $S"],
56
+ stdin=subprocess.PIPE,
57
+ stdout=subprocess.PIPE,
58
+ stderr=subprocess.STDOUT,
59
+ text=True,
60
+ bufsize=1,
61
+ )
62
+ else:
63
+ _shell_process = subprocess.Popen(
64
+ ["bash", "--noprofile", "--norc"],
65
+ stdin=subprocess.PIPE,
66
+ stdout=subprocess.PIPE,
67
+ stderr=subprocess.STDOUT,
68
+ text=True,
69
+ bufsize=1,
70
+ )
71
+
72
+ return _shell_process
73
+
74
+
75
+ def run_shell_command(command):
76
+ shell = get_shell()
77
+
78
+ command = command.replace("\r\n", "\n").replace("\r", "\n").rstrip("\n")
79
+ marker = f"__CHEMQL_END_{uuid.uuid4().hex}__"
80
+
81
+ try:
82
+ if os.name == "nt":
83
+ # Run the command, then print a unique completion marker.
84
+ shell.stdin.write(
85
+ f"{command}\r\n"
86
+ f"echo {marker}\r\n"
87
+ )
88
+ else:
89
+ shell.stdin.write( #type:ignore
90
+ f"{command}\n"
91
+ f"printf '\\n%s\\n' '{marker}'\n"
92
+ )
93
+
94
+ shell.stdin.flush() #type:ignore
95
+
96
+ output = []
97
+
98
+ while True:
99
+ line = shell.stdout.readline() #type:ignore
100
+
101
+ if not line:
102
+ return "\n".join(output) or "Shell session terminated."
103
+
104
+ line = line.rstrip("\r\n")
105
+
106
+ if marker in line:
107
+ break
108
+
109
+ output.append(line)
110
+
111
+ return "\n".join(output).strip()
112
+
113
+ except (BrokenPipeError, OSError) as exc:
114
+ return f"Shell error: {exc}"
115
+
116
+ def close_shell():
117
+ """Clean up the persistent shell on exit."""
118
+ global _shell_process
119
+
120
+ if _shell_process is not None:
121
+ if _shell_process.poll() is None:
122
+ _shell_process.terminate()
123
+ _shell_process.wait(timeout=2)
124
+
125
+ _shell_process = None
126
+
127
+
128
+ atexit.register(close_shell)
39
129
 
40
130
  class Unknown:
41
131
  """Represents a Chemql query result that cannot be resolved to a known object."""
@@ -1623,7 +1713,7 @@ def _space_query_operators(text):
1623
1713
  out.append(ch)
1624
1714
  if escaped:
1625
1715
  escaped = False
1626
- elif ch == "\\\\":
1716
+ elif ch == "\\":
1627
1717
  escaped = True
1628
1718
  elif ch == quote:
1629
1719
  quote = None
@@ -1666,7 +1756,7 @@ def _split_set_operations(text):
1666
1756
  ch = text[j]
1667
1757
  if quote:
1668
1758
  if escaped: escaped = False
1669
- elif ch == "\\\\": escaped = True
1759
+ elif ch == "\\": escaped = True
1670
1760
  elif ch == quote: quote = None
1671
1761
  elif ch in ("'", '"'): quote = ch
1672
1762
  elif ch == "(": depth += 1
@@ -2097,16 +2187,11 @@ def process_inline(text):
2097
2187
  if not stripped[end + 2:].strip():
2098
2188
  return execute_query_block(stripped[2:end].strip())
2099
2189
 
2100
- # Backtick-wrapped commands execute through subprocess (not deprecated os.system).
2190
+ # Backtick-wrapped commands use the persistent Bash session.
2101
2191
  if len(stripped) >= 2 and stripped.startswith("`") and stripped.endswith("`"):
2102
2192
  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")
2193
+ return run_shell_command(command)
2194
+
2110
2195
 
2111
2196
  # Everything else is query language.
2112
2197
  return execute_query_text(stripped)
@@ -2458,6 +2543,9 @@ def execute(arguments, query_text=None, positions=None):
2458
2543
  if arguments[0].lower() == "sorry":
2459
2544
  return "\033[34mIt's ok, everybody makes mistakes👌\033[0m"
2460
2545
 
2546
+ if arguments[0].lower() == "version":
2547
+ return __version__
2548
+
2461
2549
  if arguments[0] == "conditions":
2462
2550
  if len(arguments) != 1:
2463
2551
  return "Usage: conditions"
@@ -3042,7 +3130,7 @@ class ChemqlLexer(Lexer):
3042
3130
  first_word = words[0] if words else ""
3043
3131
 
3044
3132
  # search / source / list / return / clear / exit
3045
- if first_word in {"search", "source", "list", "clear", "exit", "findel", "findmol", "findre", "set", "add", "remove", "react", "balance", "conditions", "view", "help", "dump"}:
3133
+ if first_word in {"search", "source", "list", "clear", "exit", "findel", "findmol", "findre", "set", "add", "remove", "react", "balance", "conditions", "view", "help", "dump", "version"}:
3046
3134
  command_start = line.find(first_word)
3047
3135
  command_end = command_start + len(first_word)
3048
3136
 
@@ -3290,7 +3378,7 @@ class ChemqlCompleter(Completer):
3290
3378
  commands = (
3291
3379
  "search", "source", "list", "clear", "return", "exit",
3292
3380
  "findel", "findmol", "findre", "set", "add", "remove",
3293
- "react", "balance", "conditions",
3381
+ "react", "balance", "conditions", "version", "help", "view"
3294
3382
  )
3295
3383
 
3296
3384
  if not command_words:
@@ -3433,6 +3521,54 @@ def intersection(x, y):
3433
3521
  raise TypeError("intersection() expects two ChemQLQuery objects")
3434
3522
  return x & y
3435
3523
 
3524
+ PACKAGE_NAME = "chemql-python"
3525
+
3526
+ def upgrade_package(version=None):
3527
+ """Upgrade ChemQL, optionally to a specific version."""
3528
+
3529
+ if version is not None:
3530
+ # Accept ordinary numeric release versions, optionally with
3531
+ # prerelease or post-release suffixes.
3532
+ if not re.fullmatch(
3533
+ r"\d+(?:\.\d+)*(?:(?:a|b|rc)\d+)?(?:\.post\d+)?(?:\.dev\d+)?",
3534
+ version,
3535
+ re.IGNORECASE,
3536
+ ):
3537
+ print(f"Invalid version: {version}", file=sys.stderr)
3538
+ return 2
3539
+
3540
+ target = f"{PACKAGE_NAME}=={version}"
3541
+ print(f"Installing {target}...")
3542
+ cmd = [
3543
+ sys.executable, "-m", "pip", "install",
3544
+ "--force-reinstall", target,
3545
+ ]
3546
+ else:
3547
+ print(f"Upgrading {PACKAGE_NAME} to the latest version...")
3548
+ cmd = [
3549
+ sys.executable, "-m", "pip", "install",
3550
+ "--upgrade", PACKAGE_NAME,
3551
+ ]
3552
+
3553
+ try:
3554
+ result = subprocess.run(cmd, check=False)
3555
+ if result.returncode != 0:
3556
+ print(
3557
+ f"Installation failed (exit code {result.returncode}).",
3558
+ file=sys.stderr,
3559
+ )
3560
+ return result.returncode
3561
+
3562
+ except KeyboardInterrupt:
3563
+ print("\nUpgrade cancelled.", file=sys.stderr)
3564
+ return 130
3565
+ except OSError as exc:
3566
+ print(f"Could not run pip: {exc}", file=sys.stderr)
3567
+ return 1
3568
+
3569
+ print("ChemQL installation completed.")
3570
+ return 0
3571
+
3436
3572
 
3437
3573
  ChemQLQuery.__name__ = "ChemQLQuery"
3438
3574
  ChemQLQuery.__qualname__ = "ChemQLQuery"
@@ -3459,14 +3595,31 @@ def main():
3459
3595
  "-v",
3460
3596
  "--version",
3461
3597
  action="version",
3462
- version="1.0.0",
3598
+ version=__version__,
3463
3599
  )
3464
3600
 
3601
+
3602
+ parser.add_argument(
3603
+ "--upgrade",
3604
+ nargs="?",
3605
+ const="latest",
3606
+ default=None,
3607
+ metavar="VERSION",
3608
+ help="Upgrade ChemQL, optionally to a specific version.",
3609
+ )
3610
+
3611
+
3465
3612
  args = parser.parse_args()
3466
3613
 
3467
- if args.command:
3468
- process_lines(args.command.splitlines())
3614
+ if args.upgrade is not None:
3615
+ version_target = (
3616
+ None if args.upgrade == "latest" else args.upgrade
3617
+ )
3618
+ sys.exit(upgrade_package(version_target))
3469
3619
 
3620
+ if args.command is not None:
3621
+ process_lines(args.command.splitlines())
3622
+ return
3470
3623
  elif args.file:
3471
3624
  with open(args.file) as f:
3472
3625
  code = f.read()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chemql-python
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: A chemistry-focused query language and REPL
5
5
  Author-email: Mohammad Rakibul Islam <rakibulofficial1017@gmail.com>
6
6
  Requires-Python: >=3.10
@@ -48,6 +48,25 @@ Install the Python package from PyPI:
48
48
  pip install chemql-python
49
49
  ```
50
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
+
51
70
  ## Quick start
52
71
 
53
72
  Start the interactive terminal:
@@ -337,6 +356,57 @@ Python:
337
356
 
338
357
  Only run Python code and shell commands from sources you trust.
339
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
+
340
410
  ## Data and external services
341
411
 
342
412
  ChemQL loads bundled JSON datasets for elements, molecules, reactions,
@@ -1,4 +1,4 @@
1
- chemql/__init__.py,sha256=1lbJuAYddBEUEhTXDmgIVPVo7B616pCO-MPmf68jgvg,105921
1
+ chemql/__init__.py,sha256=qKC-xi2NrsNahVsbsPJopByzy86UzKLbzO9SWSW_N-M,109826
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-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,,
11
+ chemql_python-1.0.2.dist-info/licenses/LICENSE,sha256=o4ywHC0EwRy2zgIASiRD2sK3Vcsf7LBcTHjDvoJgRE0,1079
12
+ chemql_python-1.0.2.dist-info/METADATA,sha256=UXpbvKOb58-R9xDb2cIuAsXJp0d5bUDiNRCozeLnxes,11613
13
+ chemql_python-1.0.2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
14
+ chemql_python-1.0.2.dist-info/entry_points.txt,sha256=j98ObVWR743an524z24AiWeRsppag-py5cc7l0D0HkI,39
15
+ chemql_python-1.0.2.dist-info/top_level.txt,sha256=9UzHwnk_F6W19Q9nayaFgVyAELQXXwoYjeBgB6MWNr8,7
16
+ chemql_python-1.0.2.dist-info/RECORD,,