chemql-python 1.2.4__py3-none-any.whl → 1.2.6__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 +293 -102
- {chemql_python-1.2.4.dist-info → chemql_python-1.2.6.dist-info}/METADATA +6 -6
- {chemql_python-1.2.4.dist-info → chemql_python-1.2.6.dist-info}/RECORD +7 -7
- {chemql_python-1.2.4.dist-info → chemql_python-1.2.6.dist-info}/WHEEL +0 -0
- {chemql_python-1.2.4.dist-info → chemql_python-1.2.6.dist-info}/entry_points.txt +0 -0
- {chemql_python-1.2.4.dist-info → chemql_python-1.2.6.dist-info}/licenses/LICENSE +0 -0
- {chemql_python-1.2.4.dist-info → chemql_python-1.2.6.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.6"
|
|
51
51
|
|
|
52
52
|
# Persistent preprocessor/function state for the current ChemQL session.
|
|
53
53
|
DEFINITIONS = {}
|
|
@@ -222,11 +222,19 @@ def _call_function(name, arguments):
|
|
|
222
222
|
|
|
223
223
|
code = line[1:-1].strip()
|
|
224
224
|
|
|
225
|
+
# Function parameters must also be substituted in block
|
|
226
|
+
# headers, e.g. [for i in range(2, \1 + 1)].
|
|
227
|
+
resolved_header = (
|
|
228
|
+
"["
|
|
229
|
+
+ _expand_definitions(substitute_parameters(code))
|
|
230
|
+
+ "]"
|
|
231
|
+
)
|
|
232
|
+
|
|
225
233
|
if code.startswith("for "):
|
|
226
|
-
execute_for_block(
|
|
234
|
+
execute_for_block(resolved_header, block_body)
|
|
227
235
|
|
|
228
236
|
elif code.startswith("while "):
|
|
229
|
-
execute_while_block(
|
|
237
|
+
execute_while_block(resolved_header, block_body)
|
|
230
238
|
|
|
231
239
|
elif code.startswith("if "):
|
|
232
240
|
# Expand parameters and definitions before evaluating.
|
|
@@ -367,13 +375,13 @@ _shell_prompt_ready = False
|
|
|
367
375
|
_shell_output_streamed = False
|
|
368
376
|
_shell_output_queue = None
|
|
369
377
|
_shell_reader_thread = None
|
|
378
|
+
_shell_script_path = None
|
|
370
379
|
_shell_prompt = f"__CHEMQL_PROMPT_{uuid.uuid4().hex}__"
|
|
371
380
|
|
|
372
|
-
|
|
373
381
|
def get_shell():
|
|
374
382
|
"""Return the persistent OS shell process for the current platform."""
|
|
375
383
|
global _shell_process, _shell_master_fd, _shell_prompt_ready
|
|
376
|
-
global _shell_output_queue, _shell_reader_thread
|
|
384
|
+
global _shell_output_queue, _shell_reader_thread, _shell_script_path
|
|
377
385
|
|
|
378
386
|
if _shell_process is not None and _shell_process.poll() is None:
|
|
379
387
|
return _shell_process
|
|
@@ -382,20 +390,89 @@ def get_shell():
|
|
|
382
390
|
close_shell()
|
|
383
391
|
|
|
384
392
|
if os.name == "nt":
|
|
385
|
-
#
|
|
386
|
-
#
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
393
|
+
# PowerShell reads its program from a file, leaving stdin available for
|
|
394
|
+
# the command/marker protocol and commands such as Read-Host.
|
|
395
|
+
import tempfile
|
|
396
|
+
|
|
397
|
+
setup = r'''$ErrorActionPreference = 'Continue'
|
|
398
|
+
[Console]::InputEncoding = New-Object System.Text.UTF8Encoding($false)
|
|
399
|
+
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
|
|
400
|
+
$OutputEncoding = New-Object System.Text.UTF8Encoding($false)
|
|
401
|
+
[Console]::Out.WriteLine('__CHEMQL_READY__')
|
|
402
|
+
[Console]::Out.Flush()
|
|
403
|
+
|
|
404
|
+
while ($true) {
|
|
405
|
+
# One physical input line per request avoids command/marker line desync.
|
|
406
|
+
$request = [Console]::In.ReadLine()
|
|
407
|
+
if ($null -eq $request) { break }
|
|
408
|
+
|
|
409
|
+
$separatorIndex = $request.IndexOf('|')
|
|
410
|
+
if ($separatorIndex -le 0) {
|
|
411
|
+
[Console]::Out.WriteLine('CHEMQL_PROTOCOL_ERROR: malformed request')
|
|
412
|
+
[Console]::Out.Flush()
|
|
413
|
+
continue
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
$marker = $request.Substring(0, $separatorIndex)
|
|
417
|
+
$encodedCommand = $request.Substring($separatorIndex + 1)
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
$commandText = [System.Text.Encoding]::UTF8.GetString(
|
|
421
|
+
[System.Convert]::FromBase64String($encodedCommand)
|
|
390
422
|
)
|
|
423
|
+
if ([string]::IsNullOrWhiteSpace($commandText)) {
|
|
424
|
+
[Console]::Out.WriteLine('ChemQL received an empty PowerShell command.')
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
# Invoke-Expression runs in this persistent script scope, preserving
|
|
428
|
+
# PowerShell variables between ChemQL requests. Format pipeline
|
|
429
|
+
# output explicitly for each request; otherwise PowerShell's default
|
|
430
|
+
# formatting subsystem can defer table output until the next request,
|
|
431
|
+
# making commands such as Get-Location appear blank or duplicated.
|
|
432
|
+
$formattedOutput = Invoke-Expression -Command $commandText | Out-String -Width 4096
|
|
433
|
+
if (-not [string]::IsNullOrEmpty($formattedOutput)) {
|
|
434
|
+
[Console]::Out.Write($formattedOutput)
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
[Console]::Out.WriteLine($_.ToString())
|
|
440
|
+
}
|
|
441
|
+
finally {
|
|
442
|
+
[Console]::Out.WriteLine($marker)
|
|
443
|
+
[Console]::Out.Flush()
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
''' # I have no idea what this is, this is code I wrote today at 4:00AM but it somehow works
|
|
447
|
+
# reminder to future self to not touch this
|
|
448
|
+
fd, _shell_script_path = tempfile.mkstemp(prefix="chemql_shell_", suffix=".ps1")
|
|
449
|
+
try:
|
|
450
|
+
with os.fdopen(fd, "w", encoding="ascii", newline="\n") as script_file:
|
|
451
|
+
script_file.write(setup)
|
|
452
|
+
except Exception:
|
|
453
|
+
try:
|
|
454
|
+
os.close(fd)
|
|
455
|
+
except OSError:
|
|
456
|
+
pass
|
|
457
|
+
try:
|
|
458
|
+
os.unlink(_shell_script_path)
|
|
459
|
+
except OSError:
|
|
460
|
+
pass
|
|
461
|
+
_shell_script_path = None
|
|
462
|
+
raise
|
|
463
|
+
|
|
391
464
|
_shell_process = subprocess.Popen(
|
|
392
|
-
["powershell.exe", "-NoLogo", "-NoProfile",
|
|
465
|
+
["powershell.exe", "-NoLogo", "-NoProfile",
|
|
466
|
+
"-ExecutionPolicy", "Bypass", "-File", _shell_script_path],
|
|
393
467
|
stdin=subprocess.PIPE,
|
|
394
468
|
stdout=subprocess.PIPE,
|
|
395
469
|
stderr=subprocess.STDOUT,
|
|
396
470
|
text=True,
|
|
471
|
+
encoding="utf-8",
|
|
472
|
+
errors="replace",
|
|
397
473
|
bufsize=1,
|
|
398
474
|
)
|
|
475
|
+
|
|
399
476
|
output_queue = queue.Queue()
|
|
400
477
|
_shell_output_queue = output_queue
|
|
401
478
|
process = _shell_process
|
|
@@ -414,30 +491,67 @@ def get_shell():
|
|
|
414
491
|
|
|
415
492
|
_shell_reader_thread = threading.Thread(target=read_output, daemon=True)
|
|
416
493
|
_shell_reader_thread.start()
|
|
494
|
+
|
|
495
|
+
# Wait briefly for the PowerShell script to confirm it started. This
|
|
496
|
+
# prevents the first request from racing process startup and diagnoses
|
|
497
|
+
# startup failures instead of hanging later while waiting for a marker.
|
|
498
|
+
ready_text = ""
|
|
499
|
+
ready_deadline = time.monotonic() + 5
|
|
500
|
+
while "__CHEMQL_READY__" not in ready_text:
|
|
501
|
+
remaining = ready_deadline - time.monotonic()
|
|
502
|
+
if remaining <= 0:
|
|
503
|
+
startup_output = ready_text.replace("\\r\\n", "\\n").strip()
|
|
504
|
+
close_shell()
|
|
505
|
+
raise RuntimeError(
|
|
506
|
+
"PowerShell request loop did not start within 5 seconds."
|
|
507
|
+
+ (f" Output: {startup_output}" if startup_output else "")
|
|
508
|
+
)
|
|
509
|
+
try:
|
|
510
|
+
character = output_queue.get(timeout=min(0.1, remaining))
|
|
511
|
+
except queue.Empty:
|
|
512
|
+
if _shell_process.poll() is not None:
|
|
513
|
+
raise RuntimeError(
|
|
514
|
+
"PowerShell exited during startup with code "
|
|
515
|
+
f"{_shell_process.returncode}."
|
|
516
|
+
)
|
|
517
|
+
continue
|
|
518
|
+
if character is None:
|
|
519
|
+
raise RuntimeError("PowerShell closed its output during startup.")
|
|
520
|
+
ready_text += character
|
|
521
|
+
ready_pos = ready_text.find("__CHEMQL_READY__")
|
|
522
|
+
# Any text before the readiness token is startup diagnostics. It is
|
|
523
|
+
# not command output and should not leak into the first request.
|
|
524
|
+
startup_extra = ready_text[:ready_pos]
|
|
525
|
+
if startup_extra.strip():
|
|
526
|
+
# Preserve startup diagnostics for the next request to display.
|
|
527
|
+
for char in startup_extra:
|
|
528
|
+
output_queue.put(char)
|
|
529
|
+
|
|
530
|
+
# Windows PowerShell uses the explicit command/marker protocol above.
|
|
531
|
+
# Do not run the POSIX PTY prompt handshake below: it waits for a shell
|
|
532
|
+
# prompt that this non-interactive PowerShell script intentionally has
|
|
533
|
+
# disabled, causing get_shell() to hang before the first command.
|
|
534
|
+
return _shell_process
|
|
535
|
+
|
|
417
536
|
else:
|
|
537
|
+
# Leave the existing POSIX implementation unchanged.
|
|
418
538
|
import fcntl
|
|
419
539
|
import pty
|
|
420
540
|
import termios
|
|
421
541
|
|
|
422
542
|
master_fd, slave_fd = pty.openpty()
|
|
423
|
-
# Don't echo bytes sent by ChemQL or keyboard input twice. Keep ISIG
|
|
424
|
-
# enabled so forwarding Ctrl-C to the PTY interrupts the foreground job.
|
|
425
543
|
attrs = termios.tcgetattr(slave_fd)
|
|
426
544
|
attrs[3] &= ~termios.ECHO
|
|
427
545
|
termios.tcsetattr(slave_fd, termios.TCSANOW, attrs)
|
|
428
546
|
|
|
429
547
|
def attach_controlling_terminal():
|
|
430
|
-
# Popen(start_new_session=True) has already called setsid().
|
|
431
548
|
fcntl.ioctl(slave_fd, termios.TIOCSCTTY, 0)
|
|
432
549
|
|
|
433
550
|
try:
|
|
434
551
|
_shell_process = subprocess.Popen(
|
|
435
552
|
["bash", "--noprofile", "--norc"],
|
|
436
|
-
stdin=slave_fd,
|
|
437
|
-
|
|
438
|
-
stderr=slave_fd,
|
|
439
|
-
close_fds=True,
|
|
440
|
-
start_new_session=True,
|
|
553
|
+
stdin=slave_fd, stdout=slave_fd, stderr=slave_fd,
|
|
554
|
+
close_fds=True, start_new_session=True,
|
|
441
555
|
preexec_fn=attach_controlling_terminal,
|
|
442
556
|
env={**os.environ, "PS1": _shell_prompt, "PS2": ""},
|
|
443
557
|
)
|
|
@@ -451,7 +565,6 @@ def get_shell():
|
|
|
451
565
|
|
|
452
566
|
return _shell_process
|
|
453
567
|
|
|
454
|
-
|
|
455
568
|
def _run_shell_command_posix(command, marker=None):
|
|
456
569
|
"""Run a command through the persistent PTY and forward keyboard input.
|
|
457
570
|
|
|
@@ -469,6 +582,7 @@ def _run_shell_command_posix(command, marker=None):
|
|
|
469
582
|
prompt_bytes = _shell_prompt.encode()
|
|
470
583
|
|
|
471
584
|
def wait_for_prompt(forward_keyboard=False):
|
|
585
|
+
global _shell_output_streamed
|
|
472
586
|
collected = bytearray()
|
|
473
587
|
streamed_pos = 0
|
|
474
588
|
stdin_fd = None
|
|
@@ -481,7 +595,7 @@ def _run_shell_command_posix(command, marker=None):
|
|
|
481
595
|
while True:
|
|
482
596
|
readers = [master_fd]
|
|
483
597
|
if stdin_fd is not None:
|
|
484
|
-
readers.append(stdin_fd)
|
|
598
|
+
readers.append(stdin_fd) #type:ignore
|
|
485
599
|
try:
|
|
486
600
|
ready, _, _ = select.select(readers, [], [])
|
|
487
601
|
except KeyboardInterrupt:
|
|
@@ -562,129 +676,178 @@ def _run_shell_command_posix(command, marker=None):
|
|
|
562
676
|
close_shell()
|
|
563
677
|
return f"Shell error: {exc}"
|
|
564
678
|
|
|
565
|
-
|
|
566
679
|
def _run_shell_command_windows(command):
|
|
567
|
-
"""
|
|
568
|
-
|
|
680
|
+
"""Execute one command in the persistent PowerShell request loop."""
|
|
681
|
+
import base64
|
|
682
|
+
import msvcrt
|
|
683
|
+
import uuid
|
|
684
|
+
|
|
569
685
|
shell = get_shell()
|
|
570
686
|
output_queue = _shell_output_queue
|
|
571
687
|
if output_queue is None:
|
|
572
688
|
return "Shell error: PowerShell output reader was not initialized."
|
|
689
|
+
if shell.stdin is None:
|
|
690
|
+
return "Shell error: PowerShell stdin is unavailable."
|
|
573
691
|
|
|
574
|
-
|
|
692
|
+
marker = f"__CHEMQL_DONE_{uuid.uuid4().hex}__"
|
|
693
|
+
encoded_command = base64.b64encode(command.encode("utf-8")).decode("ascii")
|
|
694
|
+
timeout_seconds=120
|
|
695
|
+
def wait_for_marker(timeout_seconds=120):
|
|
696
|
+
"""Collect output while streaming prompts so interactive commands remain usable."""
|
|
697
|
+
global _shell_output_streamed
|
|
575
698
|
|
|
576
|
-
def wait_for_prompt(stream_output):
|
|
577
699
|
collected = ""
|
|
578
700
|
streamed_pos = 0
|
|
579
|
-
|
|
580
|
-
marker_len = len(marker)
|
|
581
|
-
stdin_is_tty = False
|
|
701
|
+
started = time.monotonic()
|
|
582
702
|
try:
|
|
583
703
|
stdin_is_tty = sys.stdin is not None and sys.stdin.isatty()
|
|
584
704
|
except (AttributeError, OSError, ValueError):
|
|
585
|
-
|
|
705
|
+
stdin_is_tty = False
|
|
586
706
|
pending_input = ""
|
|
587
707
|
|
|
708
|
+
def stream_until(end):
|
|
709
|
+
nonlocal streamed_pos
|
|
710
|
+
global _shell_output_streamed
|
|
711
|
+
if not stdin_is_tty or end <= streamed_pos:
|
|
712
|
+
return
|
|
713
|
+
visible = collected[streamed_pos:end]
|
|
714
|
+
if visible:
|
|
715
|
+
try:
|
|
716
|
+
sys.stdout.write(visible)
|
|
717
|
+
sys.stdout.flush()
|
|
718
|
+
_shell_output_streamed = True
|
|
719
|
+
except (OSError, UnicodeError):
|
|
720
|
+
pass
|
|
721
|
+
streamed_pos = end
|
|
722
|
+
|
|
588
723
|
while True:
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
724
|
+
if time.monotonic() - started > timeout_seconds:
|
|
725
|
+
return (None, collected, True)
|
|
726
|
+
|
|
592
727
|
if stdin_is_tty:
|
|
593
728
|
try:
|
|
594
|
-
while msvcrt.kbhit():
|
|
595
|
-
key = msvcrt.getwch() #type:ignore
|
|
596
|
-
if key in ("\x00", "\xe0"):
|
|
597
|
-
#
|
|
598
|
-
|
|
599
|
-
msvcrt.getwch() #type:ignore
|
|
729
|
+
while msvcrt.kbhit(): # type: ignore
|
|
730
|
+
key = msvcrt.getwch() # type: ignore
|
|
731
|
+
if key in ("\x00", "\xe0"):
|
|
732
|
+
if msvcrt.kbhit(): # type: ignore
|
|
733
|
+
msvcrt.getwch() # type: ignore
|
|
600
734
|
elif key in ("\r", "\n"):
|
|
601
|
-
shell.stdin.write(pending_input + "\
|
|
735
|
+
shell.stdin.write(pending_input + "\n") # type: ignore # text mode normalizes newlines on Windows
|
|
602
736
|
shell.stdin.flush() # type: ignore
|
|
737
|
+
sys.stdout.write("\n")
|
|
738
|
+
sys.stdout.flush()
|
|
603
739
|
pending_input = ""
|
|
604
740
|
elif key == "\b":
|
|
605
|
-
|
|
741
|
+
if pending_input:
|
|
742
|
+
pending_input = pending_input[:-1]
|
|
743
|
+
sys.stdout.write("\b \b")
|
|
744
|
+
sys.stdout.flush()
|
|
606
745
|
elif key == "\x03":
|
|
607
|
-
# Ctrl-C is sent as a line input; reset is safer than
|
|
608
|
-
# leaving ChemQL stuck in a nested interactive prompt.
|
|
609
746
|
close_shell()
|
|
610
|
-
return None
|
|
611
|
-
elif key >= " " and key != "\x7f":
|
|
747
|
+
return (None, collected, False)
|
|
748
|
+
elif key >= " " and key != "\x7f": # I have no idea what I did but it workd
|
|
612
749
|
pending_input += key
|
|
750
|
+
sys.stdout.write(key)
|
|
751
|
+
sys.stdout.flush()
|
|
613
752
|
except (OSError, ValueError):
|
|
614
753
|
stdin_is_tty = False
|
|
615
754
|
|
|
616
755
|
try:
|
|
617
756
|
character = output_queue.get(timeout=0.04)
|
|
618
757
|
except queue.Empty:
|
|
758
|
+
if shell.poll() is not None:
|
|
759
|
+
return (None, collected, False)
|
|
619
760
|
continue
|
|
620
761
|
if character is None:
|
|
621
|
-
return None
|
|
762
|
+
return (None, collected, False)
|
|
622
763
|
|
|
623
764
|
collected += character
|
|
624
765
|
marker_pos = collected.find(marker)
|
|
625
766
|
if marker_pos >= 0:
|
|
767
|
+
stream_until(marker_pos)
|
|
626
768
|
visible = collected[:marker_pos]
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
if stream_output:
|
|
634
|
-
# Stream immediately, retaining the full output for history and
|
|
635
|
-
# only withholding a suffix that could begin the prompt marker.
|
|
636
|
-
overlap = 0
|
|
637
|
-
max_overlap = min(marker_len - 1, len(collected))
|
|
638
|
-
for size in range(max_overlap, 0, -1):
|
|
639
|
-
if collected.endswith(marker[:size]):
|
|
640
|
-
overlap = size
|
|
641
|
-
break
|
|
642
|
-
safe_end = len(collected) - overlap
|
|
643
|
-
if safe_end > streamed_pos:
|
|
644
|
-
sys.stdout.write(collected[streamed_pos:safe_end])
|
|
645
|
-
sys.stdout.flush()
|
|
646
|
-
_shell_output_streamed = True
|
|
647
|
-
streamed_pos = safe_end
|
|
769
|
+
return (
|
|
770
|
+
visible.replace("\r\n", "\n").rstrip("\r\n "),
|
|
771
|
+
collected,
|
|
772
|
+
False,
|
|
773
|
+
)
|
|
648
774
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
775
|
+
# Hold back a suffix that could be the beginning of the completion
|
|
776
|
+
# marker, so protocol text is never accidentally printed.
|
|
777
|
+
overlap = min(len(marker) - 1, len(collected))
|
|
778
|
+
while overlap and not collected.endswith(marker[:overlap]):
|
|
779
|
+
overlap -= 1
|
|
780
|
+
stream_until(len(collected) - overlap)
|
|
655
781
|
|
|
656
782
|
try:
|
|
657
|
-
shell.stdin.write(
|
|
658
|
-
shell.stdin.flush()
|
|
659
|
-
result =
|
|
783
|
+
shell.stdin.write(marker + "|" + encoded_command + "\n") # text mode normalizes newlines on Windows
|
|
784
|
+
shell.stdin.flush()
|
|
785
|
+
result, collected, timed_out = wait_for_marker()
|
|
786
|
+
if timed_out:
|
|
787
|
+
close_shell()
|
|
788
|
+
debug = collected.replace("\r\n", "\n").rstrip()
|
|
789
|
+
detail = f"PowerShell did not return its completion marker within {timeout_seconds} seconds."
|
|
790
|
+
if debug:
|
|
791
|
+
detail += f"\nOutput received before timeout:\n{debug}"
|
|
792
|
+
else:
|
|
793
|
+
detail += "\nNo output was received. The PowerShell request loop may not have started or may not be reading stdin."
|
|
794
|
+
return detail
|
|
660
795
|
if result is None:
|
|
661
796
|
close_shell()
|
|
662
|
-
return "Shell session terminated."
|
|
797
|
+
return "Shell session terminated before PowerShell returned its completion marker."
|
|
663
798
|
return result
|
|
664
799
|
except (BrokenPipeError, OSError, ValueError) as exc:
|
|
665
800
|
close_shell()
|
|
666
801
|
return f"Shell error: {exc}"
|
|
667
802
|
|
|
668
|
-
|
|
669
803
|
def run_shell_command(command):
|
|
670
804
|
global _shell_output_streamed
|
|
671
|
-
|
|
805
|
+
|
|
806
|
+
command = (
|
|
807
|
+
command.replace("\r\n", "\n")
|
|
808
|
+
.replace("\r", "\n")
|
|
809
|
+
.rstrip("\n")
|
|
810
|
+
)
|
|
811
|
+
|
|
672
812
|
try:
|
|
673
813
|
if os.name == "nt":
|
|
674
|
-
|
|
675
|
-
|
|
814
|
+
stripped = command.strip()
|
|
815
|
+
|
|
816
|
+
if stripped.casefold() in {
|
|
817
|
+
"powershell",
|
|
818
|
+
"powershell.exe",
|
|
819
|
+
"pwsh",
|
|
820
|
+
"pwsh.exe",
|
|
821
|
+
}:
|
|
822
|
+
return (
|
|
823
|
+
"ChemQL already uses PowerShell on Windows; "
|
|
824
|
+
"run the command directly instead of starting "
|
|
825
|
+
"a nested shell."
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
# Clear the terminal without sending Clear-Host to PowerShell.
|
|
829
|
+
if stripped.casefold() in {
|
|
830
|
+
"clear",
|
|
831
|
+
"cls",
|
|
832
|
+
"clear-host",
|
|
833
|
+
}:
|
|
834
|
+
os.system("cls")
|
|
835
|
+
return ""
|
|
836
|
+
|
|
676
837
|
_shell_output_streamed = False
|
|
677
838
|
return _run_shell_command_windows(command)
|
|
839
|
+
|
|
840
|
+
# POSIX behavior remains unchanged.
|
|
678
841
|
return _run_shell_command_posix(command)
|
|
842
|
+
|
|
679
843
|
except KeyboardInterrupt:
|
|
680
844
|
close_shell()
|
|
681
845
|
return "Shell command interrupted. Shell session was reset."
|
|
682
846
|
|
|
683
|
-
|
|
684
847
|
def close_shell():
|
|
685
848
|
"""Clean up the persistent shell and its pseudo-terminal on exit."""
|
|
686
849
|
global _shell_process, _shell_master_fd, _shell_prompt_ready
|
|
687
|
-
global _shell_output_queue, _shell_reader_thread
|
|
850
|
+
global _shell_output_queue, _shell_reader_thread, _shell_script_path
|
|
688
851
|
|
|
689
852
|
if _shell_process is not None:
|
|
690
853
|
if _shell_process.poll() is None:
|
|
@@ -709,6 +872,13 @@ def close_shell():
|
|
|
709
872
|
_shell_output_queue = None
|
|
710
873
|
_shell_reader_thread = None
|
|
711
874
|
|
|
875
|
+
if _shell_script_path is not None:
|
|
876
|
+
try:
|
|
877
|
+
os.unlink(_shell_script_path)
|
|
878
|
+
except OSError:
|
|
879
|
+
pass
|
|
880
|
+
_shell_script_path = None
|
|
881
|
+
|
|
712
882
|
|
|
713
883
|
atexit.register(close_shell)
|
|
714
884
|
|
|
@@ -2317,6 +2487,24 @@ def _space_query_operators(text):
|
|
|
2317
2487
|
out.append(ch)
|
|
2318
2488
|
i += 1
|
|
2319
2489
|
continue
|
|
2490
|
+
# Preserve the compound operator "like!" as a single token.
|
|
2491
|
+
if text[i:i + 5].casefold() == "like!":
|
|
2492
|
+
before_ok = (
|
|
2493
|
+
i == 0 or not (
|
|
2494
|
+
text[i - 1].isalnum() or text[i - 1] == "_"
|
|
2495
|
+
)
|
|
2496
|
+
)
|
|
2497
|
+
end = i + 5
|
|
2498
|
+
after_ok = (
|
|
2499
|
+
end == len(text) or not (
|
|
2500
|
+
text[end].isalnum() or text[end] == "_"
|
|
2501
|
+
)
|
|
2502
|
+
)
|
|
2503
|
+
|
|
2504
|
+
if before_ok and after_ok:
|
|
2505
|
+
out.extend((" ", text[i:end], " "))
|
|
2506
|
+
i = end
|
|
2507
|
+
continue
|
|
2320
2508
|
if ch in "()":
|
|
2321
2509
|
out.extend((" ", ch, " "))
|
|
2322
2510
|
i += 1
|
|
@@ -3111,7 +3299,7 @@ def process_lines(lines):
|
|
|
3111
3299
|
result = strip_ansi(result) if NO_ANSI else result
|
|
3112
3300
|
# POSIX PTY commands stream output live. Never print their returned
|
|
3113
3301
|
# capture a second time; Windows pipe mode still needs normal printing.
|
|
3114
|
-
if result and
|
|
3302
|
+
if result and not _shell_output_streamed:
|
|
3115
3303
|
print(result)
|
|
3116
3304
|
_record_execution("```\n" + command + "\n```", result, not result.startswith("Shell error:"))
|
|
3117
3305
|
globals()["_shell_output_streamed"] = False
|
|
@@ -3526,10 +3714,13 @@ def execute(arguments, query_text=None, positions=None):
|
|
|
3526
3714
|
# Convenience aliases for very common lookups.
|
|
3527
3715
|
# These are syntactic sugar for normal search queries.
|
|
3528
3716
|
if arguments[0] == "findel":
|
|
3529
|
-
if len(arguments)
|
|
3717
|
+
if len(arguments) < 2:
|
|
3530
3718
|
return "Usage: findel <name-or-symbol>"
|
|
3531
3719
|
|
|
3532
3720
|
value = arguments[1]
|
|
3721
|
+
|
|
3722
|
+
return_attributes = None
|
|
3723
|
+
|
|
3533
3724
|
return execute(
|
|
3534
3725
|
["search", "elements", "name", "like", value, "or", "symbol", "like", value]
|
|
3535
3726
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chemql-python
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.6
|
|
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.6 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.6\
|
|
28
|
+
> The examples below describe the ChemQL 1.2.6 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.6 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.6
|
|
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=6bmYNdAj5SEZzsrKVtmVUqn5PB-XSIT4bkMmj4BjY_0,167346
|
|
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.6.dist-info/licenses/LICENSE,sha256=o4ywHC0EwRy2zgIASiRD2sK3Vcsf7LBcTHjDvoJgRE0,1079
|
|
12
|
+
chemql_python-1.2.6.dist-info/METADATA,sha256=KENM4stpgADfHjbgSEBkNZLSI4a6AWOAylgUnl4wMAw,30529
|
|
13
|
+
chemql_python-1.2.6.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
14
|
+
chemql_python-1.2.6.dist-info/entry_points.txt,sha256=j98ObVWR743an524z24AiWeRsppag-py5cc7l0D0HkI,39
|
|
15
|
+
chemql_python-1.2.6.dist-info/top_level.txt,sha256=9UzHwnk_F6W19Q9nayaFgVyAELQXXwoYjeBgB6MWNr8,7
|
|
16
|
+
chemql_python-1.2.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|