chemql-python 1.2.4__tar.gz → 1.2.5__tar.gz
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_python-1.2.4/src/chemql_python.egg-info → chemql_python-1.2.5}/PKG-INFO +6 -6
- {chemql_python-1.2.4 → chemql_python-1.2.5}/README.md +5 -5
- {chemql_python-1.2.4 → chemql_python-1.2.5}/pyproject.toml +1 -1
- {chemql_python-1.2.4 → chemql_python-1.2.5}/src/chemql/__init__.py +283 -100
- {chemql_python-1.2.4 → chemql_python-1.2.5/src/chemql_python.egg-info}/PKG-INFO +6 -6
- {chemql_python-1.2.4 → chemql_python-1.2.5}/LICENSE +0 -0
- {chemql_python-1.2.4 → chemql_python-1.2.5}/setup.cfg +0 -0
- {chemql_python-1.2.4 → chemql_python-1.2.5}/src/chemql/__main__.py +0 -0
- {chemql_python-1.2.4 → chemql_python-1.2.5}/src/chemql/data/BondEnergies.json +0 -0
- {chemql_python-1.2.4 → chemql_python-1.2.5}/src/chemql/data/MoleculeBonds.json +0 -0
- {chemql_python-1.2.4 → chemql_python-1.2.5}/src/chemql/data/Molecules.json +0 -0
- {chemql_python-1.2.4 → chemql_python-1.2.5}/src/chemql/data/PeriodicTableJSON.json +0 -0
- {chemql_python-1.2.4 → chemql_python-1.2.5}/src/chemql/data/Reactions.json +0 -0
- {chemql_python-1.2.4 → chemql_python-1.2.5}/src/chemql/data/THIRD_PARTY_LICENSES/PeriodicTableJSON.txt +0 -0
- {chemql_python-1.2.4 → chemql_python-1.2.5}/src/chemql/data/THIRD_PARTY_LICENSES/RHEA.txt +0 -0
- {chemql_python-1.2.4 → chemql_python-1.2.5}/src/chemql/html/periodic_table.html +0 -0
- {chemql_python-1.2.4 → chemql_python-1.2.5}/src/chemql_python.egg-info/SOURCES.txt +0 -0
- {chemql_python-1.2.4 → chemql_python-1.2.5}/src/chemql_python.egg-info/dependency_links.txt +0 -0
- {chemql_python-1.2.4 → chemql_python-1.2.5}/src/chemql_python.egg-info/entry_points.txt +0 -0
- {chemql_python-1.2.4 → chemql_python-1.2.5}/src/chemql_python.egg-info/requires.txt +0 -0
- {chemql_python-1.2.4 → chemql_python-1.2.5}/src/chemql_python.egg-info/top_level.txt +0 -0
- {chemql_python-1.2.4 → chemql_python-1.2.5}/tests/test_external_molecule_lookup.py +0 -0
- {chemql_python-1.2.4 → chemql_python-1.2.5}/tests/test_public_api.py +0 -0
- {chemql_python-1.2.4 → chemql_python-1.2.5}/tests/test_reactions.py +0 -0
|
@@ -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
|
|
@@ -4,15 +4,15 @@
|
|
|
4
4
|
shell commands in one interactive environment.**
|
|
5
5
|
|
|
6
6
|
ChemQL is a command-line chemistry query language for exploring chemical
|
|
7
|
-
elements, molecules, and reactions. Version 1.2.
|
|
7
|
+
elements, molecules, and reactions. Version 1.2.4 combines database-style
|
|
8
8
|
chemistry queries, reaction tools, Python interoperability, reusable functions
|
|
9
9
|
and macros, and a persistent shell in one scriptable environment. It combines a small SQL-inspired
|
|
10
10
|
query syntax with a persistent Python environment, reaction utilities,
|
|
11
11
|
optional external molecule lookup, and tools for running repeatable
|
|
12
12
|
scripts.
|
|
13
13
|
|
|
14
|
-
> **Version covered by this README:** 1.2.
|
|
15
|
-
> The examples below describe the ChemQL 1.2.
|
|
14
|
+
> **Version covered by this README:** 1.2.4\
|
|
15
|
+
> The examples below describe the ChemQL 1.2.4 source. Dataset
|
|
16
16
|
> contents and external-service availability can affect individual
|
|
17
17
|
> lookup results.
|
|
18
18
|
|
|
@@ -525,7 +525,7 @@ queried or inspected from Python.
|
|
|
525
525
|
|
|
526
526
|
## Preprocessor, functions, and macros
|
|
527
527
|
|
|
528
|
-
ChemQL 1.2.
|
|
528
|
+
ChemQL 1.2.5 adds three ways to reuse values and logic. These declarations
|
|
529
529
|
are processed in source order: definitions affect later lines, not earlier
|
|
530
530
|
ones. They persist during the current ChemQL process/session; they are not
|
|
531
531
|
saved automatically between separate launches.
|
|
@@ -820,7 +820,7 @@ redirect a file into the command.
|
|
|
820
820
|
``` bash
|
|
821
821
|
chemql --version
|
|
822
822
|
chemql --upgrade
|
|
823
|
-
chemql --upgrade 1.2.
|
|
823
|
+
chemql --upgrade 1.2.4
|
|
824
824
|
```
|
|
825
825
|
|
|
826
826
|
`--upgrade` installs the latest published `chemql-python` release; supplying a
|
|
@@ -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 = {}
|
|
@@ -367,13 +367,13 @@ _shell_prompt_ready = False
|
|
|
367
367
|
_shell_output_streamed = False
|
|
368
368
|
_shell_output_queue = None
|
|
369
369
|
_shell_reader_thread = None
|
|
370
|
+
_shell_script_path = None
|
|
370
371
|
_shell_prompt = f"__CHEMQL_PROMPT_{uuid.uuid4().hex}__"
|
|
371
372
|
|
|
372
|
-
|
|
373
373
|
def get_shell():
|
|
374
374
|
"""Return the persistent OS shell process for the current platform."""
|
|
375
375
|
global _shell_process, _shell_master_fd, _shell_prompt_ready
|
|
376
|
-
global _shell_output_queue, _shell_reader_thread
|
|
376
|
+
global _shell_output_queue, _shell_reader_thread, _shell_script_path
|
|
377
377
|
|
|
378
378
|
if _shell_process is not None and _shell_process.poll() is None:
|
|
379
379
|
return _shell_process
|
|
@@ -382,20 +382,89 @@ def get_shell():
|
|
|
382
382
|
close_shell()
|
|
383
383
|
|
|
384
384
|
if os.name == "nt":
|
|
385
|
-
#
|
|
386
|
-
#
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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)
|
|
390
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
|
+
|
|
391
456
|
_shell_process = subprocess.Popen(
|
|
392
|
-
["powershell.exe", "-NoLogo", "-NoProfile",
|
|
457
|
+
["powershell.exe", "-NoLogo", "-NoProfile",
|
|
458
|
+
"-ExecutionPolicy", "Bypass", "-File", _shell_script_path],
|
|
393
459
|
stdin=subprocess.PIPE,
|
|
394
460
|
stdout=subprocess.PIPE,
|
|
395
461
|
stderr=subprocess.STDOUT,
|
|
396
462
|
text=True,
|
|
463
|
+
encoding="utf-8",
|
|
464
|
+
errors="replace",
|
|
397
465
|
bufsize=1,
|
|
398
466
|
)
|
|
467
|
+
|
|
399
468
|
output_queue = queue.Queue()
|
|
400
469
|
_shell_output_queue = output_queue
|
|
401
470
|
process = _shell_process
|
|
@@ -414,30 +483,67 @@ def get_shell():
|
|
|
414
483
|
|
|
415
484
|
_shell_reader_thread = threading.Thread(target=read_output, daemon=True)
|
|
416
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
|
+
|
|
417
528
|
else:
|
|
529
|
+
# Leave the existing POSIX implementation unchanged.
|
|
418
530
|
import fcntl
|
|
419
531
|
import pty
|
|
420
532
|
import termios
|
|
421
533
|
|
|
422
534
|
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
535
|
attrs = termios.tcgetattr(slave_fd)
|
|
426
536
|
attrs[3] &= ~termios.ECHO
|
|
427
537
|
termios.tcsetattr(slave_fd, termios.TCSANOW, attrs)
|
|
428
538
|
|
|
429
539
|
def attach_controlling_terminal():
|
|
430
|
-
# Popen(start_new_session=True) has already called setsid().
|
|
431
540
|
fcntl.ioctl(slave_fd, termios.TIOCSCTTY, 0)
|
|
432
541
|
|
|
433
542
|
try:
|
|
434
543
|
_shell_process = subprocess.Popen(
|
|
435
544
|
["bash", "--noprofile", "--norc"],
|
|
436
|
-
stdin=slave_fd,
|
|
437
|
-
|
|
438
|
-
stderr=slave_fd,
|
|
439
|
-
close_fds=True,
|
|
440
|
-
start_new_session=True,
|
|
545
|
+
stdin=slave_fd, stdout=slave_fd, stderr=slave_fd,
|
|
546
|
+
close_fds=True, start_new_session=True,
|
|
441
547
|
preexec_fn=attach_controlling_terminal,
|
|
442
548
|
env={**os.environ, "PS1": _shell_prompt, "PS2": ""},
|
|
443
549
|
)
|
|
@@ -451,7 +557,6 @@ def get_shell():
|
|
|
451
557
|
|
|
452
558
|
return _shell_process
|
|
453
559
|
|
|
454
|
-
|
|
455
560
|
def _run_shell_command_posix(command, marker=None):
|
|
456
561
|
"""Run a command through the persistent PTY and forward keyboard input.
|
|
457
562
|
|
|
@@ -469,6 +574,7 @@ def _run_shell_command_posix(command, marker=None):
|
|
|
469
574
|
prompt_bytes = _shell_prompt.encode()
|
|
470
575
|
|
|
471
576
|
def wait_for_prompt(forward_keyboard=False):
|
|
577
|
+
global _shell_output_streamed
|
|
472
578
|
collected = bytearray()
|
|
473
579
|
streamed_pos = 0
|
|
474
580
|
stdin_fd = None
|
|
@@ -481,7 +587,7 @@ def _run_shell_command_posix(command, marker=None):
|
|
|
481
587
|
while True:
|
|
482
588
|
readers = [master_fd]
|
|
483
589
|
if stdin_fd is not None:
|
|
484
|
-
readers.append(stdin_fd)
|
|
590
|
+
readers.append(stdin_fd) #type:ignore
|
|
485
591
|
try:
|
|
486
592
|
ready, _, _ = select.select(readers, [], [])
|
|
487
593
|
except KeyboardInterrupt:
|
|
@@ -562,129 +668,178 @@ def _run_shell_command_posix(command, marker=None):
|
|
|
562
668
|
close_shell()
|
|
563
669
|
return f"Shell error: {exc}"
|
|
564
670
|
|
|
565
|
-
|
|
566
671
|
def _run_shell_command_windows(command):
|
|
567
|
-
"""
|
|
568
|
-
|
|
672
|
+
"""Execute one command in the persistent PowerShell request loop."""
|
|
673
|
+
import base64
|
|
674
|
+
import msvcrt
|
|
675
|
+
import uuid
|
|
676
|
+
|
|
569
677
|
shell = get_shell()
|
|
570
678
|
output_queue = _shell_output_queue
|
|
571
679
|
if output_queue is None:
|
|
572
680
|
return "Shell error: PowerShell output reader was not initialized."
|
|
681
|
+
if shell.stdin is None:
|
|
682
|
+
return "Shell error: PowerShell stdin is unavailable."
|
|
573
683
|
|
|
574
|
-
|
|
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
|
|
575
690
|
|
|
576
|
-
def wait_for_prompt(stream_output):
|
|
577
691
|
collected = ""
|
|
578
692
|
streamed_pos = 0
|
|
579
|
-
|
|
580
|
-
marker_len = len(marker)
|
|
581
|
-
stdin_is_tty = False
|
|
693
|
+
started = time.monotonic()
|
|
582
694
|
try:
|
|
583
695
|
stdin_is_tty = sys.stdin is not None and sys.stdin.isatty()
|
|
584
696
|
except (AttributeError, OSError, ValueError):
|
|
585
|
-
|
|
697
|
+
stdin_is_tty = False
|
|
586
698
|
pending_input = ""
|
|
587
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
|
+
|
|
588
715
|
while True:
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
716
|
+
if time.monotonic() - started > timeout_seconds:
|
|
717
|
+
return (None, collected, True)
|
|
718
|
+
|
|
592
719
|
if stdin_is_tty:
|
|
593
720
|
try:
|
|
594
|
-
while msvcrt.kbhit():
|
|
595
|
-
key = msvcrt.getwch() #type:ignore
|
|
596
|
-
if key in ("\x00", "\xe0"):
|
|
597
|
-
#
|
|
598
|
-
|
|
599
|
-
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
|
|
600
726
|
elif key in ("\r", "\n"):
|
|
601
|
-
shell.stdin.write(pending_input + "\
|
|
727
|
+
shell.stdin.write(pending_input + "\n") # type: ignore # text mode normalizes newlines on Windows
|
|
602
728
|
shell.stdin.flush() # type: ignore
|
|
729
|
+
sys.stdout.write("\n")
|
|
730
|
+
sys.stdout.flush()
|
|
603
731
|
pending_input = ""
|
|
604
732
|
elif key == "\b":
|
|
605
|
-
|
|
733
|
+
if pending_input:
|
|
734
|
+
pending_input = pending_input[:-1]
|
|
735
|
+
sys.stdout.write("\b \b")
|
|
736
|
+
sys.stdout.flush()
|
|
606
737
|
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
738
|
close_shell()
|
|
610
|
-
return None
|
|
611
|
-
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
|
|
612
741
|
pending_input += key
|
|
742
|
+
sys.stdout.write(key)
|
|
743
|
+
sys.stdout.flush()
|
|
613
744
|
except (OSError, ValueError):
|
|
614
745
|
stdin_is_tty = False
|
|
615
746
|
|
|
616
747
|
try:
|
|
617
748
|
character = output_queue.get(timeout=0.04)
|
|
618
749
|
except queue.Empty:
|
|
750
|
+
if shell.poll() is not None:
|
|
751
|
+
return (None, collected, False)
|
|
619
752
|
continue
|
|
620
753
|
if character is None:
|
|
621
|
-
return None
|
|
754
|
+
return (None, collected, False)
|
|
622
755
|
|
|
623
756
|
collected += character
|
|
624
757
|
marker_pos = collected.find(marker)
|
|
625
758
|
if marker_pos >= 0:
|
|
759
|
+
stream_until(marker_pos)
|
|
626
760
|
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
|
|
761
|
+
return (
|
|
762
|
+
visible.replace("\r\n", "\n").rstrip("\r\n "),
|
|
763
|
+
collected,
|
|
764
|
+
False,
|
|
765
|
+
)
|
|
648
766
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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)
|
|
655
773
|
|
|
656
774
|
try:
|
|
657
|
-
shell.stdin.write(
|
|
658
|
-
shell.stdin.flush()
|
|
659
|
-
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
|
|
660
787
|
if result is None:
|
|
661
788
|
close_shell()
|
|
662
|
-
return "Shell session terminated."
|
|
789
|
+
return "Shell session terminated before PowerShell returned its completion marker."
|
|
663
790
|
return result
|
|
664
791
|
except (BrokenPipeError, OSError, ValueError) as exc:
|
|
665
792
|
close_shell()
|
|
666
793
|
return f"Shell error: {exc}"
|
|
667
794
|
|
|
668
|
-
|
|
669
795
|
def run_shell_command(command):
|
|
670
796
|
global _shell_output_streamed
|
|
671
|
-
|
|
797
|
+
|
|
798
|
+
command = (
|
|
799
|
+
command.replace("\r\n", "\n")
|
|
800
|
+
.replace("\r", "\n")
|
|
801
|
+
.rstrip("\n")
|
|
802
|
+
)
|
|
803
|
+
|
|
672
804
|
try:
|
|
673
805
|
if os.name == "nt":
|
|
674
|
-
|
|
675
|
-
|
|
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
|
+
|
|
676
829
|
_shell_output_streamed = False
|
|
677
830
|
return _run_shell_command_windows(command)
|
|
831
|
+
|
|
832
|
+
# POSIX behavior remains unchanged.
|
|
678
833
|
return _run_shell_command_posix(command)
|
|
834
|
+
|
|
679
835
|
except KeyboardInterrupt:
|
|
680
836
|
close_shell()
|
|
681
837
|
return "Shell command interrupted. Shell session was reset."
|
|
682
838
|
|
|
683
|
-
|
|
684
839
|
def close_shell():
|
|
685
840
|
"""Clean up the persistent shell and its pseudo-terminal on exit."""
|
|
686
841
|
global _shell_process, _shell_master_fd, _shell_prompt_ready
|
|
687
|
-
global _shell_output_queue, _shell_reader_thread
|
|
842
|
+
global _shell_output_queue, _shell_reader_thread, _shell_script_path
|
|
688
843
|
|
|
689
844
|
if _shell_process is not None:
|
|
690
845
|
if _shell_process.poll() is None:
|
|
@@ -709,6 +864,13 @@ def close_shell():
|
|
|
709
864
|
_shell_output_queue = None
|
|
710
865
|
_shell_reader_thread = None
|
|
711
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
|
+
|
|
712
874
|
|
|
713
875
|
atexit.register(close_shell)
|
|
714
876
|
|
|
@@ -2317,6 +2479,24 @@ def _space_query_operators(text):
|
|
|
2317
2479
|
out.append(ch)
|
|
2318
2480
|
i += 1
|
|
2319
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
|
|
2320
2500
|
if ch in "()":
|
|
2321
2501
|
out.extend((" ", ch, " "))
|
|
2322
2502
|
i += 1
|
|
@@ -3111,7 +3291,7 @@ def process_lines(lines):
|
|
|
3111
3291
|
result = strip_ansi(result) if NO_ANSI else result
|
|
3112
3292
|
# POSIX PTY commands stream output live. Never print their returned
|
|
3113
3293
|
# capture a second time; Windows pipe mode still needs normal printing.
|
|
3114
|
-
if result and
|
|
3294
|
+
if result and not _shell_output_streamed:
|
|
3115
3295
|
print(result)
|
|
3116
3296
|
_record_execution("```\n" + command + "\n```", result, not result.startswith("Shell error:"))
|
|
3117
3297
|
globals()["_shell_output_streamed"] = False
|
|
@@ -3526,10 +3706,13 @@ def execute(arguments, query_text=None, positions=None):
|
|
|
3526
3706
|
# Convenience aliases for very common lookups.
|
|
3527
3707
|
# These are syntactic sugar for normal search queries.
|
|
3528
3708
|
if arguments[0] == "findel":
|
|
3529
|
-
if len(arguments)
|
|
3709
|
+
if len(arguments) < 2:
|
|
3530
3710
|
return "Usage: findel <name-or-symbol>"
|
|
3531
3711
|
|
|
3532
3712
|
value = arguments[1]
|
|
3713
|
+
|
|
3714
|
+
return_attributes = None
|
|
3715
|
+
|
|
3533
3716
|
return execute(
|
|
3534
3717
|
["search", "elements", "name", "like", value, "or", "symbol", "like", value]
|
|
3535
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|