chemql-python 1.2.3__py3-none-any.whl → 1.2.5__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 +320 -110
- {chemql_python-1.2.3.dist-info → chemql_python-1.2.5.dist-info}/METADATA +6 -6
- {chemql_python-1.2.3.dist-info → chemql_python-1.2.5.dist-info}/RECORD +7 -7
- {chemql_python-1.2.3.dist-info → chemql_python-1.2.5.dist-info}/WHEEL +0 -0
- {chemql_python-1.2.3.dist-info → chemql_python-1.2.5.dist-info}/entry_points.txt +0 -0
- {chemql_python-1.2.3.dist-info → chemql_python-1.2.5.dist-info}/licenses/LICENSE +0 -0
- {chemql_python-1.2.3.dist-info → chemql_python-1.2.5.dist-info}/top_level.txt +0 -0
chemql/__init__.py
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import math
|
|
3
|
-
import shlex
|
|
4
|
-
import os
|
|
5
|
-
import re
|
|
6
|
-
import ast
|
|
7
1
|
import argparse
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import sys
|
|
2
|
+
import ast
|
|
3
|
+
import atexit
|
|
11
4
|
import contextlib
|
|
5
|
+
from fractions import Fraction
|
|
6
|
+
from functools import reduce
|
|
12
7
|
import io
|
|
8
|
+
import json
|
|
9
|
+
import math
|
|
10
|
+
from math import gcd
|
|
11
|
+
import os
|
|
13
12
|
from pathlib import Path
|
|
14
13
|
from prompt_toolkit import PromptSession
|
|
15
14
|
from prompt_toolkit.key_binding import KeyBindings
|
|
16
15
|
from prompt_toolkit.keys import Keys
|
|
17
16
|
from prompt_toolkit.lexers import Lexer
|
|
18
|
-
import atexit
|
|
19
|
-
import uuid
|
|
20
|
-
import select
|
|
21
|
-
import queue
|
|
22
|
-
import threading
|
|
23
17
|
from prompt_toolkit.styles import Style
|
|
24
18
|
from prompt_toolkit.completion import Completer, Completion
|
|
19
|
+
import queue
|
|
20
|
+
import re
|
|
21
|
+
import shlex
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
24
|
+
import select
|
|
25
|
+
import time
|
|
26
|
+
import threading
|
|
27
|
+
import uuid
|
|
25
28
|
import urllib.parse
|
|
26
29
|
import urllib.request
|
|
27
30
|
import urllib.error
|
|
28
|
-
from fractions import Fraction
|
|
29
|
-
from functools import reduce
|
|
30
|
-
from math import gcd
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
__all__ = [
|
|
@@ -47,7 +47,7 @@ __all__ = [
|
|
|
47
47
|
"__version__"
|
|
48
48
|
]
|
|
49
49
|
|
|
50
|
-
__version__ = "1.2.
|
|
50
|
+
__version__ = "1.2.5"
|
|
51
51
|
|
|
52
52
|
# Persistent preprocessor/function state for the current ChemQL session.
|
|
53
53
|
DEFINITIONS = {}
|
|
@@ -108,6 +108,18 @@ def _split_function_arguments(source):
|
|
|
108
108
|
if tail or source.strip():
|
|
109
109
|
args.append(tail)
|
|
110
110
|
return args
|
|
111
|
+
def is_block_header(line):
|
|
112
|
+
"""Return True only for actual ChemQL block headers."""
|
|
113
|
+
if not (line.startswith("[") and line.endswith("]")):
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
code = line[1:-1].strip()
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
code.startswith("if ")
|
|
120
|
+
or code.startswith("for ")
|
|
121
|
+
or code.startswith("while ")
|
|
122
|
+
)
|
|
111
123
|
|
|
112
124
|
def _call_function(name, arguments):
|
|
113
125
|
"""Call a ChemQL function, including let assignments and block syntax."""
|
|
@@ -194,13 +206,13 @@ def _call_function(name, arguments):
|
|
|
194
206
|
|
|
195
207
|
# Recognize block headers generically, while excluding
|
|
196
208
|
# closing and branch markers from being treated as headers.
|
|
197
|
-
generic_header = re.match(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
)
|
|
209
|
+
# generic_header = re.match(
|
|
210
|
+
# r"^\[\s*(?!endblock\b|else\b|elif\b)"
|
|
211
|
+
# r"[A-Za-z_]\w*\b.*\]\s*$",
|
|
212
|
+
# line,
|
|
213
|
+
# )
|
|
202
214
|
|
|
203
|
-
if
|
|
215
|
+
if is_block_header(line):
|
|
204
216
|
end = find_endblock(lines, i + 1)
|
|
205
217
|
|
|
206
218
|
block_body = [
|
|
@@ -355,13 +367,13 @@ _shell_prompt_ready = False
|
|
|
355
367
|
_shell_output_streamed = False
|
|
356
368
|
_shell_output_queue = None
|
|
357
369
|
_shell_reader_thread = None
|
|
370
|
+
_shell_script_path = None
|
|
358
371
|
_shell_prompt = f"__CHEMQL_PROMPT_{uuid.uuid4().hex}__"
|
|
359
372
|
|
|
360
|
-
|
|
361
373
|
def get_shell():
|
|
362
374
|
"""Return the persistent OS shell process for the current platform."""
|
|
363
375
|
global _shell_process, _shell_master_fd, _shell_prompt_ready
|
|
364
|
-
global _shell_output_queue, _shell_reader_thread
|
|
376
|
+
global _shell_output_queue, _shell_reader_thread, _shell_script_path
|
|
365
377
|
|
|
366
378
|
if _shell_process is not None and _shell_process.poll() is None:
|
|
367
379
|
return _shell_process
|
|
@@ -370,20 +382,89 @@ def get_shell():
|
|
|
370
382
|
close_shell()
|
|
371
383
|
|
|
372
384
|
if os.name == "nt":
|
|
373
|
-
#
|
|
374
|
-
#
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
385
|
+
# PowerShell reads its program from a file, leaving stdin available for
|
|
386
|
+
# the command/marker protocol and commands such as Read-Host.
|
|
387
|
+
import tempfile
|
|
388
|
+
|
|
389
|
+
setup = r'''$ErrorActionPreference = 'Continue'
|
|
390
|
+
[Console]::InputEncoding = New-Object System.Text.UTF8Encoding($false)
|
|
391
|
+
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
|
|
392
|
+
$OutputEncoding = New-Object System.Text.UTF8Encoding($false)
|
|
393
|
+
[Console]::Out.WriteLine('__CHEMQL_READY__')
|
|
394
|
+
[Console]::Out.Flush()
|
|
395
|
+
|
|
396
|
+
while ($true) {
|
|
397
|
+
# One physical input line per request avoids command/marker line desync.
|
|
398
|
+
$request = [Console]::In.ReadLine()
|
|
399
|
+
if ($null -eq $request) { break }
|
|
400
|
+
|
|
401
|
+
$separatorIndex = $request.IndexOf('|')
|
|
402
|
+
if ($separatorIndex -le 0) {
|
|
403
|
+
[Console]::Out.WriteLine('CHEMQL_PROTOCOL_ERROR: malformed request')
|
|
404
|
+
[Console]::Out.Flush()
|
|
405
|
+
continue
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
$marker = $request.Substring(0, $separatorIndex)
|
|
409
|
+
$encodedCommand = $request.Substring($separatorIndex + 1)
|
|
410
|
+
|
|
411
|
+
try {
|
|
412
|
+
$commandText = [System.Text.Encoding]::UTF8.GetString(
|
|
413
|
+
[System.Convert]::FromBase64String($encodedCommand)
|
|
378
414
|
)
|
|
415
|
+
if ([string]::IsNullOrWhiteSpace($commandText)) {
|
|
416
|
+
[Console]::Out.WriteLine('ChemQL received an empty PowerShell command.')
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
# Invoke-Expression runs in this persistent script scope, preserving
|
|
420
|
+
# PowerShell variables between ChemQL requests. Format pipeline
|
|
421
|
+
# output explicitly for each request; otherwise PowerShell's default
|
|
422
|
+
# formatting subsystem can defer table output until the next request,
|
|
423
|
+
# making commands such as Get-Location appear blank or duplicated.
|
|
424
|
+
$formattedOutput = Invoke-Expression -Command $commandText | Out-String -Width 4096
|
|
425
|
+
if (-not [string]::IsNullOrEmpty($formattedOutput)) {
|
|
426
|
+
[Console]::Out.Write($formattedOutput)
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
[Console]::Out.WriteLine($_.ToString())
|
|
432
|
+
}
|
|
433
|
+
finally {
|
|
434
|
+
[Console]::Out.WriteLine($marker)
|
|
435
|
+
[Console]::Out.Flush()
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
''' # I have no idea what this is, this is code I wrote today at 4:00AM but it somehow works
|
|
439
|
+
# reminder to future self to not touch this
|
|
440
|
+
fd, _shell_script_path = tempfile.mkstemp(prefix="chemql_shell_", suffix=".ps1")
|
|
441
|
+
try:
|
|
442
|
+
with os.fdopen(fd, "w", encoding="ascii", newline="\n") as script_file:
|
|
443
|
+
script_file.write(setup)
|
|
444
|
+
except Exception:
|
|
445
|
+
try:
|
|
446
|
+
os.close(fd)
|
|
447
|
+
except OSError:
|
|
448
|
+
pass
|
|
449
|
+
try:
|
|
450
|
+
os.unlink(_shell_script_path)
|
|
451
|
+
except OSError:
|
|
452
|
+
pass
|
|
453
|
+
_shell_script_path = None
|
|
454
|
+
raise
|
|
455
|
+
|
|
379
456
|
_shell_process = subprocess.Popen(
|
|
380
|
-
["powershell.exe", "-NoLogo", "-NoProfile",
|
|
457
|
+
["powershell.exe", "-NoLogo", "-NoProfile",
|
|
458
|
+
"-ExecutionPolicy", "Bypass", "-File", _shell_script_path],
|
|
381
459
|
stdin=subprocess.PIPE,
|
|
382
460
|
stdout=subprocess.PIPE,
|
|
383
461
|
stderr=subprocess.STDOUT,
|
|
384
462
|
text=True,
|
|
463
|
+
encoding="utf-8",
|
|
464
|
+
errors="replace",
|
|
385
465
|
bufsize=1,
|
|
386
466
|
)
|
|
467
|
+
|
|
387
468
|
output_queue = queue.Queue()
|
|
388
469
|
_shell_output_queue = output_queue
|
|
389
470
|
process = _shell_process
|
|
@@ -402,30 +483,67 @@ def get_shell():
|
|
|
402
483
|
|
|
403
484
|
_shell_reader_thread = threading.Thread(target=read_output, daemon=True)
|
|
404
485
|
_shell_reader_thread.start()
|
|
486
|
+
|
|
487
|
+
# Wait briefly for the PowerShell script to confirm it started. This
|
|
488
|
+
# prevents the first request from racing process startup and diagnoses
|
|
489
|
+
# startup failures instead of hanging later while waiting for a marker.
|
|
490
|
+
ready_text = ""
|
|
491
|
+
ready_deadline = time.monotonic() + 5
|
|
492
|
+
while "__CHEMQL_READY__" not in ready_text:
|
|
493
|
+
remaining = ready_deadline - time.monotonic()
|
|
494
|
+
if remaining <= 0:
|
|
495
|
+
startup_output = ready_text.replace("\\r\\n", "\\n").strip()
|
|
496
|
+
close_shell()
|
|
497
|
+
raise RuntimeError(
|
|
498
|
+
"PowerShell request loop did not start within 5 seconds."
|
|
499
|
+
+ (f" Output: {startup_output}" if startup_output else "")
|
|
500
|
+
)
|
|
501
|
+
try:
|
|
502
|
+
character = output_queue.get(timeout=min(0.1, remaining))
|
|
503
|
+
except queue.Empty:
|
|
504
|
+
if _shell_process.poll() is not None:
|
|
505
|
+
raise RuntimeError(
|
|
506
|
+
"PowerShell exited during startup with code "
|
|
507
|
+
f"{_shell_process.returncode}."
|
|
508
|
+
)
|
|
509
|
+
continue
|
|
510
|
+
if character is None:
|
|
511
|
+
raise RuntimeError("PowerShell closed its output during startup.")
|
|
512
|
+
ready_text += character
|
|
513
|
+
ready_pos = ready_text.find("__CHEMQL_READY__")
|
|
514
|
+
# Any text before the readiness token is startup diagnostics. It is
|
|
515
|
+
# not command output and should not leak into the first request.
|
|
516
|
+
startup_extra = ready_text[:ready_pos]
|
|
517
|
+
if startup_extra.strip():
|
|
518
|
+
# Preserve startup diagnostics for the next request to display.
|
|
519
|
+
for char in startup_extra:
|
|
520
|
+
output_queue.put(char)
|
|
521
|
+
|
|
522
|
+
# Windows PowerShell uses the explicit command/marker protocol above.
|
|
523
|
+
# Do not run the POSIX PTY prompt handshake below: it waits for a shell
|
|
524
|
+
# prompt that this non-interactive PowerShell script intentionally has
|
|
525
|
+
# disabled, causing get_shell() to hang before the first command.
|
|
526
|
+
return _shell_process
|
|
527
|
+
|
|
405
528
|
else:
|
|
529
|
+
# Leave the existing POSIX implementation unchanged.
|
|
406
530
|
import fcntl
|
|
407
531
|
import pty
|
|
408
532
|
import termios
|
|
409
533
|
|
|
410
534
|
master_fd, slave_fd = pty.openpty()
|
|
411
|
-
# Don't echo bytes sent by ChemQL or keyboard input twice. Keep ISIG
|
|
412
|
-
# enabled so forwarding Ctrl-C to the PTY interrupts the foreground job.
|
|
413
535
|
attrs = termios.tcgetattr(slave_fd)
|
|
414
536
|
attrs[3] &= ~termios.ECHO
|
|
415
537
|
termios.tcsetattr(slave_fd, termios.TCSANOW, attrs)
|
|
416
538
|
|
|
417
539
|
def attach_controlling_terminal():
|
|
418
|
-
# Popen(start_new_session=True) has already called setsid().
|
|
419
540
|
fcntl.ioctl(slave_fd, termios.TIOCSCTTY, 0)
|
|
420
541
|
|
|
421
542
|
try:
|
|
422
543
|
_shell_process = subprocess.Popen(
|
|
423
544
|
["bash", "--noprofile", "--norc"],
|
|
424
|
-
stdin=slave_fd,
|
|
425
|
-
|
|
426
|
-
stderr=slave_fd,
|
|
427
|
-
close_fds=True,
|
|
428
|
-
start_new_session=True,
|
|
545
|
+
stdin=slave_fd, stdout=slave_fd, stderr=slave_fd,
|
|
546
|
+
close_fds=True, start_new_session=True,
|
|
429
547
|
preexec_fn=attach_controlling_terminal,
|
|
430
548
|
env={**os.environ, "PS1": _shell_prompt, "PS2": ""},
|
|
431
549
|
)
|
|
@@ -439,7 +557,6 @@ def get_shell():
|
|
|
439
557
|
|
|
440
558
|
return _shell_process
|
|
441
559
|
|
|
442
|
-
|
|
443
560
|
def _run_shell_command_posix(command, marker=None):
|
|
444
561
|
"""Run a command through the persistent PTY and forward keyboard input.
|
|
445
562
|
|
|
@@ -457,6 +574,7 @@ def _run_shell_command_posix(command, marker=None):
|
|
|
457
574
|
prompt_bytes = _shell_prompt.encode()
|
|
458
575
|
|
|
459
576
|
def wait_for_prompt(forward_keyboard=False):
|
|
577
|
+
global _shell_output_streamed
|
|
460
578
|
collected = bytearray()
|
|
461
579
|
streamed_pos = 0
|
|
462
580
|
stdin_fd = None
|
|
@@ -469,7 +587,7 @@ def _run_shell_command_posix(command, marker=None):
|
|
|
469
587
|
while True:
|
|
470
588
|
readers = [master_fd]
|
|
471
589
|
if stdin_fd is not None:
|
|
472
|
-
readers.append(stdin_fd)
|
|
590
|
+
readers.append(stdin_fd) #type:ignore
|
|
473
591
|
try:
|
|
474
592
|
ready, _, _ = select.select(readers, [], [])
|
|
475
593
|
except KeyboardInterrupt:
|
|
@@ -550,129 +668,178 @@ def _run_shell_command_posix(command, marker=None):
|
|
|
550
668
|
close_shell()
|
|
551
669
|
return f"Shell error: {exc}"
|
|
552
670
|
|
|
553
|
-
|
|
554
671
|
def _run_shell_command_windows(command):
|
|
555
|
-
"""
|
|
556
|
-
|
|
672
|
+
"""Execute one command in the persistent PowerShell request loop."""
|
|
673
|
+
import base64
|
|
674
|
+
import msvcrt
|
|
675
|
+
import uuid
|
|
676
|
+
|
|
557
677
|
shell = get_shell()
|
|
558
678
|
output_queue = _shell_output_queue
|
|
559
679
|
if output_queue is None:
|
|
560
680
|
return "Shell error: PowerShell output reader was not initialized."
|
|
681
|
+
if shell.stdin is None:
|
|
682
|
+
return "Shell error: PowerShell stdin is unavailable."
|
|
561
683
|
|
|
562
|
-
|
|
684
|
+
marker = f"__CHEMQL_DONE_{uuid.uuid4().hex}__"
|
|
685
|
+
encoded_command = base64.b64encode(command.encode("utf-8")).decode("ascii")
|
|
686
|
+
timeout_seconds=120
|
|
687
|
+
def wait_for_marker(timeout_seconds=120):
|
|
688
|
+
"""Collect output while streaming prompts so interactive commands remain usable."""
|
|
689
|
+
global _shell_output_streamed
|
|
563
690
|
|
|
564
|
-
def wait_for_prompt(stream_output):
|
|
565
691
|
collected = ""
|
|
566
692
|
streamed_pos = 0
|
|
567
|
-
|
|
568
|
-
marker_len = len(marker)
|
|
569
|
-
stdin_is_tty = False
|
|
693
|
+
started = time.monotonic()
|
|
570
694
|
try:
|
|
571
695
|
stdin_is_tty = sys.stdin is not None and sys.stdin.isatty()
|
|
572
696
|
except (AttributeError, OSError, ValueError):
|
|
573
|
-
|
|
697
|
+
stdin_is_tty = False
|
|
574
698
|
pending_input = ""
|
|
575
699
|
|
|
700
|
+
def stream_until(end):
|
|
701
|
+
nonlocal streamed_pos
|
|
702
|
+
global _shell_output_streamed
|
|
703
|
+
if not stdin_is_tty or end <= streamed_pos:
|
|
704
|
+
return
|
|
705
|
+
visible = collected[streamed_pos:end]
|
|
706
|
+
if visible:
|
|
707
|
+
try:
|
|
708
|
+
sys.stdout.write(visible)
|
|
709
|
+
sys.stdout.flush()
|
|
710
|
+
_shell_output_streamed = True
|
|
711
|
+
except (OSError, UnicodeError):
|
|
712
|
+
pass
|
|
713
|
+
streamed_pos = end
|
|
714
|
+
|
|
576
715
|
while True:
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
716
|
+
if time.monotonic() - started > timeout_seconds:
|
|
717
|
+
return (None, collected, True)
|
|
718
|
+
|
|
580
719
|
if stdin_is_tty:
|
|
581
720
|
try:
|
|
582
|
-
while msvcrt.kbhit():
|
|
583
|
-
key = msvcrt.getwch() #type:ignore
|
|
584
|
-
if key in ("\x00", "\xe0"):
|
|
585
|
-
#
|
|
586
|
-
|
|
587
|
-
msvcrt.getwch() #type:ignore
|
|
721
|
+
while msvcrt.kbhit(): # type: ignore
|
|
722
|
+
key = msvcrt.getwch() # type: ignore
|
|
723
|
+
if key in ("\x00", "\xe0"):
|
|
724
|
+
if msvcrt.kbhit(): # type: ignore
|
|
725
|
+
msvcrt.getwch() # type: ignore
|
|
588
726
|
elif key in ("\r", "\n"):
|
|
589
|
-
shell.stdin.write(pending_input + "\
|
|
727
|
+
shell.stdin.write(pending_input + "\n") # type: ignore # text mode normalizes newlines on Windows
|
|
590
728
|
shell.stdin.flush() # type: ignore
|
|
729
|
+
sys.stdout.write("\n")
|
|
730
|
+
sys.stdout.flush()
|
|
591
731
|
pending_input = ""
|
|
592
732
|
elif key == "\b":
|
|
593
|
-
|
|
733
|
+
if pending_input:
|
|
734
|
+
pending_input = pending_input[:-1]
|
|
735
|
+
sys.stdout.write("\b \b")
|
|
736
|
+
sys.stdout.flush()
|
|
594
737
|
elif key == "\x03":
|
|
595
|
-
# Ctrl-C is sent as a line input; reset is safer than
|
|
596
|
-
# leaving ChemQL stuck in a nested interactive prompt.
|
|
597
738
|
close_shell()
|
|
598
|
-
return None
|
|
599
|
-
elif key >= " " and key != "\x7f":
|
|
739
|
+
return (None, collected, False)
|
|
740
|
+
elif key >= " " and key != "\x7f": # I have no idea what I did but it workd
|
|
600
741
|
pending_input += key
|
|
742
|
+
sys.stdout.write(key)
|
|
743
|
+
sys.stdout.flush()
|
|
601
744
|
except (OSError, ValueError):
|
|
602
745
|
stdin_is_tty = False
|
|
603
746
|
|
|
604
747
|
try:
|
|
605
748
|
character = output_queue.get(timeout=0.04)
|
|
606
749
|
except queue.Empty:
|
|
750
|
+
if shell.poll() is not None:
|
|
751
|
+
return (None, collected, False)
|
|
607
752
|
continue
|
|
608
753
|
if character is None:
|
|
609
|
-
return None
|
|
754
|
+
return (None, collected, False)
|
|
610
755
|
|
|
611
756
|
collected += character
|
|
612
757
|
marker_pos = collected.find(marker)
|
|
613
758
|
if marker_pos >= 0:
|
|
759
|
+
stream_until(marker_pos)
|
|
614
760
|
visible = collected[:marker_pos]
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
if stream_output:
|
|
622
|
-
# Stream immediately, retaining the full output for history and
|
|
623
|
-
# only withholding a suffix that could begin the prompt marker.
|
|
624
|
-
overlap = 0
|
|
625
|
-
max_overlap = min(marker_len - 1, len(collected))
|
|
626
|
-
for size in range(max_overlap, 0, -1):
|
|
627
|
-
if collected.endswith(marker[:size]):
|
|
628
|
-
overlap = size
|
|
629
|
-
break
|
|
630
|
-
safe_end = len(collected) - overlap
|
|
631
|
-
if safe_end > streamed_pos:
|
|
632
|
-
sys.stdout.write(collected[streamed_pos:safe_end])
|
|
633
|
-
sys.stdout.flush()
|
|
634
|
-
_shell_output_streamed = True
|
|
635
|
-
streamed_pos = safe_end
|
|
761
|
+
return (
|
|
762
|
+
visible.replace("\r\n", "\n").rstrip("\r\n "),
|
|
763
|
+
collected,
|
|
764
|
+
False,
|
|
765
|
+
)
|
|
636
766
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
767
|
+
# Hold back a suffix that could be the beginning of the completion
|
|
768
|
+
# marker, so protocol text is never accidentally printed.
|
|
769
|
+
overlap = min(len(marker) - 1, len(collected))
|
|
770
|
+
while overlap and not collected.endswith(marker[:overlap]):
|
|
771
|
+
overlap -= 1
|
|
772
|
+
stream_until(len(collected) - overlap)
|
|
643
773
|
|
|
644
774
|
try:
|
|
645
|
-
shell.stdin.write(
|
|
646
|
-
shell.stdin.flush()
|
|
647
|
-
result =
|
|
775
|
+
shell.stdin.write(marker + "|" + encoded_command + "\n") # text mode normalizes newlines on Windows
|
|
776
|
+
shell.stdin.flush()
|
|
777
|
+
result, collected, timed_out = wait_for_marker()
|
|
778
|
+
if timed_out:
|
|
779
|
+
close_shell()
|
|
780
|
+
debug = collected.replace("\r\n", "\n").rstrip()
|
|
781
|
+
detail = f"PowerShell did not return its completion marker within {timeout_seconds} seconds."
|
|
782
|
+
if debug:
|
|
783
|
+
detail += f"\nOutput received before timeout:\n{debug}"
|
|
784
|
+
else:
|
|
785
|
+
detail += "\nNo output was received. The PowerShell request loop may not have started or may not be reading stdin."
|
|
786
|
+
return detail
|
|
648
787
|
if result is None:
|
|
649
788
|
close_shell()
|
|
650
|
-
return "Shell session terminated."
|
|
789
|
+
return "Shell session terminated before PowerShell returned its completion marker."
|
|
651
790
|
return result
|
|
652
791
|
except (BrokenPipeError, OSError, ValueError) as exc:
|
|
653
792
|
close_shell()
|
|
654
793
|
return f"Shell error: {exc}"
|
|
655
794
|
|
|
656
|
-
|
|
657
795
|
def run_shell_command(command):
|
|
658
796
|
global _shell_output_streamed
|
|
659
|
-
|
|
797
|
+
|
|
798
|
+
command = (
|
|
799
|
+
command.replace("\r\n", "\n")
|
|
800
|
+
.replace("\r", "\n")
|
|
801
|
+
.rstrip("\n")
|
|
802
|
+
)
|
|
803
|
+
|
|
660
804
|
try:
|
|
661
805
|
if os.name == "nt":
|
|
662
|
-
|
|
663
|
-
|
|
806
|
+
stripped = command.strip()
|
|
807
|
+
|
|
808
|
+
if stripped.casefold() in {
|
|
809
|
+
"powershell",
|
|
810
|
+
"powershell.exe",
|
|
811
|
+
"pwsh",
|
|
812
|
+
"pwsh.exe",
|
|
813
|
+
}:
|
|
814
|
+
return (
|
|
815
|
+
"ChemQL already uses PowerShell on Windows; "
|
|
816
|
+
"run the command directly instead of starting "
|
|
817
|
+
"a nested shell."
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
# Clear the terminal without sending Clear-Host to PowerShell.
|
|
821
|
+
if stripped.casefold() in {
|
|
822
|
+
"clear",
|
|
823
|
+
"cls",
|
|
824
|
+
"clear-host",
|
|
825
|
+
}:
|
|
826
|
+
os.system("cls")
|
|
827
|
+
return ""
|
|
828
|
+
|
|
664
829
|
_shell_output_streamed = False
|
|
665
830
|
return _run_shell_command_windows(command)
|
|
831
|
+
|
|
832
|
+
# POSIX behavior remains unchanged.
|
|
666
833
|
return _run_shell_command_posix(command)
|
|
834
|
+
|
|
667
835
|
except KeyboardInterrupt:
|
|
668
836
|
close_shell()
|
|
669
837
|
return "Shell command interrupted. Shell session was reset."
|
|
670
838
|
|
|
671
|
-
|
|
672
839
|
def close_shell():
|
|
673
840
|
"""Clean up the persistent shell and its pseudo-terminal on exit."""
|
|
674
841
|
global _shell_process, _shell_master_fd, _shell_prompt_ready
|
|
675
|
-
global _shell_output_queue, _shell_reader_thread
|
|
842
|
+
global _shell_output_queue, _shell_reader_thread, _shell_script_path
|
|
676
843
|
|
|
677
844
|
if _shell_process is not None:
|
|
678
845
|
if _shell_process.poll() is None:
|
|
@@ -697,6 +864,13 @@ def close_shell():
|
|
|
697
864
|
_shell_output_queue = None
|
|
698
865
|
_shell_reader_thread = None
|
|
699
866
|
|
|
867
|
+
if _shell_script_path is not None:
|
|
868
|
+
try:
|
|
869
|
+
os.unlink(_shell_script_path)
|
|
870
|
+
except OSError:
|
|
871
|
+
pass
|
|
872
|
+
_shell_script_path = None
|
|
873
|
+
|
|
700
874
|
|
|
701
875
|
atexit.register(close_shell)
|
|
702
876
|
|
|
@@ -2305,6 +2479,24 @@ def _space_query_operators(text):
|
|
|
2305
2479
|
out.append(ch)
|
|
2306
2480
|
i += 1
|
|
2307
2481
|
continue
|
|
2482
|
+
# Preserve the compound operator "like!" as a single token.
|
|
2483
|
+
if text[i:i + 5].casefold() == "like!":
|
|
2484
|
+
before_ok = (
|
|
2485
|
+
i == 0 or not (
|
|
2486
|
+
text[i - 1].isalnum() or text[i - 1] == "_"
|
|
2487
|
+
)
|
|
2488
|
+
)
|
|
2489
|
+
end = i + 5
|
|
2490
|
+
after_ok = (
|
|
2491
|
+
end == len(text) or not (
|
|
2492
|
+
text[end].isalnum() or text[end] == "_"
|
|
2493
|
+
)
|
|
2494
|
+
)
|
|
2495
|
+
|
|
2496
|
+
if before_ok and after_ok:
|
|
2497
|
+
out.extend((" ", text[i:end], " "))
|
|
2498
|
+
i = end
|
|
2499
|
+
continue
|
|
2308
2500
|
if ch in "()":
|
|
2309
2501
|
out.extend((" ", ch, " "))
|
|
2310
2502
|
i += 1
|
|
@@ -2509,15 +2701,30 @@ def find_endblock(lines, start):
|
|
|
2509
2701
|
for i in range(start, len(lines)):
|
|
2510
2702
|
stripped = lines[i].strip()
|
|
2511
2703
|
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
elif stripped == "[endblock]":
|
|
2704
|
+
# Closing block.
|
|
2705
|
+
if stripped == "[endblock]":
|
|
2516
2706
|
depth -= 1
|
|
2517
2707
|
|
|
2518
2708
|
if depth == 0:
|
|
2519
2709
|
return i
|
|
2520
2710
|
|
|
2711
|
+
continue
|
|
2712
|
+
|
|
2713
|
+
# Branch markers do not start new blocks.
|
|
2714
|
+
if stripped in ("[else]", "[endif]", "[endfor]", "[endwhile]"):
|
|
2715
|
+
continue
|
|
2716
|
+
|
|
2717
|
+
# Recognize both validated Python headers and generic
|
|
2718
|
+
# ChemQL block headers, consistently with _call_function().
|
|
2719
|
+
generic_header = re.match(
|
|
2720
|
+
r"^\[\s*(?!endblock\b|else\b|elif\b)"
|
|
2721
|
+
r"[A-Za-z_]\w*\b.*\]\s*$",
|
|
2722
|
+
stripped,
|
|
2723
|
+
)
|
|
2724
|
+
|
|
2725
|
+
if is_block_header(stripped):
|
|
2726
|
+
depth += 1
|
|
2727
|
+
|
|
2521
2728
|
raise SyntaxError("Missing `[endblock]`")
|
|
2522
2729
|
|
|
2523
2730
|
def remove_comments(line):
|
|
@@ -3084,7 +3291,7 @@ def process_lines(lines):
|
|
|
3084
3291
|
result = strip_ansi(result) if NO_ANSI else result
|
|
3085
3292
|
# POSIX PTY commands stream output live. Never print their returned
|
|
3086
3293
|
# capture a second time; Windows pipe mode still needs normal printing.
|
|
3087
|
-
if result and
|
|
3294
|
+
if result and not _shell_output_streamed:
|
|
3088
3295
|
print(result)
|
|
3089
3296
|
_record_execution("```\n" + command + "\n```", result, not result.startswith("Shell error:"))
|
|
3090
3297
|
globals()["_shell_output_streamed"] = False
|
|
@@ -3499,10 +3706,13 @@ def execute(arguments, query_text=None, positions=None):
|
|
|
3499
3706
|
# Convenience aliases for very common lookups.
|
|
3500
3707
|
# These are syntactic sugar for normal search queries.
|
|
3501
3708
|
if arguments[0] == "findel":
|
|
3502
|
-
if len(arguments)
|
|
3709
|
+
if len(arguments) < 2:
|
|
3503
3710
|
return "Usage: findel <name-or-symbol>"
|
|
3504
3711
|
|
|
3505
3712
|
value = arguments[1]
|
|
3713
|
+
|
|
3714
|
+
return_attributes = None
|
|
3715
|
+
|
|
3506
3716
|
return execute(
|
|
3507
3717
|
["search", "elements", "name", "like", value, "or", "symbol", "like", value]
|
|
3508
3718
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chemql-python
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.5
|
|
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
|
|
@@ -17,15 +17,15 @@ Dynamic: license-file
|
|
|
17
17
|
shell commands in one interactive environment.**
|
|
18
18
|
|
|
19
19
|
ChemQL is a command-line chemistry query language for exploring chemical
|
|
20
|
-
elements, molecules, and reactions. Version 1.2.
|
|
20
|
+
elements, molecules, and reactions. Version 1.2.4 combines database-style
|
|
21
21
|
chemistry queries, reaction tools, Python interoperability, reusable functions
|
|
22
22
|
and macros, and a persistent shell in one scriptable environment. It combines a small SQL-inspired
|
|
23
23
|
query syntax with a persistent Python environment, reaction utilities,
|
|
24
24
|
optional external molecule lookup, and tools for running repeatable
|
|
25
25
|
scripts.
|
|
26
26
|
|
|
27
|
-
> **Version covered by this README:** 1.2.
|
|
28
|
-
> The examples below describe the ChemQL 1.2.
|
|
27
|
+
> **Version covered by this README:** 1.2.4\
|
|
28
|
+
> The examples below describe the ChemQL 1.2.4 source. Dataset
|
|
29
29
|
> contents and external-service availability can affect individual
|
|
30
30
|
> lookup results.
|
|
31
31
|
|
|
@@ -538,7 +538,7 @@ queried or inspected from Python.
|
|
|
538
538
|
|
|
539
539
|
## Preprocessor, functions, and macros
|
|
540
540
|
|
|
541
|
-
ChemQL 1.2.
|
|
541
|
+
ChemQL 1.2.5 adds three ways to reuse values and logic. These declarations
|
|
542
542
|
are processed in source order: definitions affect later lines, not earlier
|
|
543
543
|
ones. They persist during the current ChemQL process/session; they are not
|
|
544
544
|
saved automatically between separate launches.
|
|
@@ -833,7 +833,7 @@ redirect a file into the command.
|
|
|
833
833
|
``` bash
|
|
834
834
|
chemql --version
|
|
835
835
|
chemql --upgrade
|
|
836
|
-
chemql --upgrade 1.2.
|
|
836
|
+
chemql --upgrade 1.2.4
|
|
837
837
|
```
|
|
838
838
|
|
|
839
839
|
`--upgrade` installs the latest published `chemql-python` release; supplying a
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
chemql/__init__.py,sha256=
|
|
1
|
+
chemql/__init__.py,sha256=WmZWHZgR-ZnVebISvUxSNyfjDVXbo8v8LeMDYwYZlFk,167015
|
|
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.2.
|
|
12
|
-
chemql_python-1.2.
|
|
13
|
-
chemql_python-1.2.
|
|
14
|
-
chemql_python-1.2.
|
|
15
|
-
chemql_python-1.2.
|
|
16
|
-
chemql_python-1.2.
|
|
11
|
+
chemql_python-1.2.5.dist-info/licenses/LICENSE,sha256=o4ywHC0EwRy2zgIASiRD2sK3Vcsf7LBcTHjDvoJgRE0,1079
|
|
12
|
+
chemql_python-1.2.5.dist-info/METADATA,sha256=mYrZXpJiCjQDgFiBY2Ili4aCwlK-E-h0jBZFRjxbXoM,30529
|
|
13
|
+
chemql_python-1.2.5.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
14
|
+
chemql_python-1.2.5.dist-info/entry_points.txt,sha256=j98ObVWR743an524z24AiWeRsppag-py5cc7l0D0HkI,39
|
|
15
|
+
chemql_python-1.2.5.dist-info/top_level.txt,sha256=9UzHwnk_F6W19Q9nayaFgVyAELQXXwoYjeBgB6MWNr8,7
|
|
16
|
+
chemql_python-1.2.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|