chemql-python 1.2.1__py3-none-any.whl → 1.2.3__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
@@ -18,6 +18,8 @@ from prompt_toolkit.lexers import Lexer
18
18
  import atexit
19
19
  import uuid
20
20
  import select
21
+ import queue
22
+ import threading
21
23
  from prompt_toolkit.styles import Style
22
24
  from prompt_toolkit.completion import Completer, Completion
23
25
  import urllib.parse
@@ -45,7 +47,7 @@ __all__ = [
45
47
  "__version__"
46
48
  ]
47
49
 
48
- __version__ = "1.2.1"
50
+ __version__ = "1.2.3"
49
51
 
50
52
  # Persistent preprocessor/function state for the current ChemQL session.
51
53
  DEFINITIONS = {}
@@ -53,6 +55,7 @@ FUNCTIONS = {}
53
55
  MACROS = {}
54
56
  EXECUTION_HISTORY = []
55
57
  _DEFINITION_NAME = r"[A-Za-z_][A-Za-z0-9_]*"
58
+ _NO_RETURN = object()
56
59
 
57
60
  def _expand_definitions(line):
58
61
  """Replace @NAME literally, including inside strings, using current definitions."""
@@ -107,111 +110,203 @@ def _split_function_arguments(source):
107
110
  return args
108
111
 
109
112
  def _call_function(name, arguments):
110
- """Call a ChemQL function, including `let` assignments and block syntax."""
113
+ """Call a ChemQL function, including let assignments and block syntax."""
111
114
  if name not in FUNCTIONS:
112
115
  raise NameError(f"Unknown ChemQL function `{name}`")
113
116
 
114
117
  values = [evaluate_python(arg) for arg in arguments]
115
118
  definition = FUNCTIONS[name]
119
+
116
120
  saved_globals = python_globals.copy()
117
121
  saved_chemql_variables = CHEMQL_VARIABLES.copy()
122
+
118
123
  had_arguments = "arguments" in python_globals
119
124
  saved_arguments = python_globals.get("arguments")
120
- # `arguments` contains only the supplied arguments, not the function name.
125
+
126
+ # Expose only the supplied arguments.
121
127
  python_globals["arguments"] = list(values)
122
128
 
123
129
  def substitute_parameters(source):
124
- # Replace positional placeholders as tokens, so \1 does not corrupt \10.
130
+ """Replace \\1, \\2, etc. without corrupting \\10 or longer indices."""
125
131
  return re.sub(
126
132
  r"\\(\d+)",
127
- lambda match: repr(values[int(match.group(1)) - 1])
128
- if 0 < int(match.group(1)) <= len(values) else match.group(0),
133
+ lambda match: (
134
+ repr(values[int(match.group(1)) - 1])
135
+ if 0 < int(match.group(1)) <= len(values)
136
+ else match.group(0)
137
+ ),
129
138
  source,
130
139
  )
131
140
 
132
141
  def normalize_let_line(source):
133
- """Translate `let name = expression` to a Python assignment block."""
134
- match = re.fullmatch(r"\s*let\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*", source)
142
+ """Translate let name = expression into a Python assignment."""
143
+ match = re.fullmatch(
144
+ r"\s*let\s+([A-Za-z_]\w*)\s*=\s*(.*?)\s*",
145
+ source,
146
+ )
147
+
135
148
  if not match:
136
149
  return source
150
+
137
151
  variable, expression = match.groups()
138
152
  expression = substitute_parameters(expression)
139
- # In ChemQL function expressions, [i] means evaluate i, rather than
140
- # create a Python list containing i. Keep this deliberately simple and
141
- # avoid changing arbitrary Python list literals.
153
+ expression = _expand_definitions(expression)
154
+
155
+ # Translate simple ChemQL [variable] expressions into (variable).
142
156
  expression = re.sub(
143
- r"\[\s*([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)\s*\]",
157
+ r"\[\s*([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s*\]",
144
158
  r"(\1)",
145
159
  expression,
146
160
  )
161
+
147
162
  return f"[{variable} = {expression}]"
148
163
 
149
- try:
150
- body = definition["body"]
164
+ def execute_function_lines(lines):
165
+ """Execute function statements and propagate return values."""
151
166
  i = 0
152
- while i < len(body):
153
- line = body[i].strip()
167
+
168
+ while i < len(lines):
169
+ line = lines[i].strip()
170
+
154
171
  if not line:
155
172
  i += 1
156
173
  continue
157
174
 
158
- # A function may contain a multiline [[[ Python block ]]].
175
+ # Multiline Python block.
159
176
  if line == "[[[":
160
177
  block = []
161
178
  i += 1
162
- while i < len(body) and body[i].strip() != "]]]":
163
- block.append(substitute_parameters(body[i]))
179
+
180
+ while i < len(lines) and lines[i].strip() != "]]]":
181
+ source_line = substitute_parameters(lines[i])
182
+ source_line = _expand_definitions(source_line)
183
+ block.append(source_line)
164
184
  i += 1
165
- if i >= len(body):
166
- raise SyntaxError(f"Missing `]]]` in function `{name}`")
185
+
186
+ if i >= len(lines):
187
+ raise SyntaxError(
188
+ f"Missing `]]]` in function `{name}`"
189
+ )
190
+
167
191
  evaluate_python("\n".join(block))
168
192
  i += 1
169
193
  continue
170
194
 
171
- # Support ChemQL's [for ...], [if ...], and [while ...] blocks
172
- # inside functions, using the same block executors as the REPL.
173
- if is_python_block_header(line):
174
- end = find_endblock(body, i + 1)
195
+ # Recognize block headers generically, while excluding
196
+ # closing and branch markers from being treated as headers.
197
+ generic_header = re.match(
198
+ r"^\[\s*(?!endblock\b|else\b|elif\b)"
199
+ r"[A-Za-z_]\w*\b.*\]\s*$",
200
+ line,
201
+ )
202
+
203
+ if is_python_block_header(line) or generic_header:
204
+ end = find_endblock(lines, i + 1)
205
+
175
206
  block_body = [
176
207
  normalize_let_line(substitute_parameters(source_line))
177
- for source_line in body[i + 1:end]
208
+ for source_line in lines[i + 1:end]
178
209
  ]
210
+
179
211
  code = line[1:-1].strip()
212
+
180
213
  if code.startswith("for "):
181
214
  execute_for_block(line, block_body)
215
+
182
216
  elif code.startswith("while "):
183
217
  execute_while_block(line, block_body)
218
+
184
219
  elif code.startswith("if "):
185
- execute_if_block(line, block_body)
220
+ # Expand parameters and definitions before evaluating.
221
+ condition = code[3:].strip()
222
+ condition = substitute_parameters(condition)
223
+ condition = _expand_definitions(condition)
224
+
225
+ # Find the top-level else branch.
226
+ else_index = next(
227
+ (
228
+ j
229
+ for j, source_line in enumerate(block_body)
230
+ if source_line.strip() == "[else]"
231
+ ),
232
+ None,
233
+ )
234
+
235
+ if evaluate_python(condition):
236
+ selected_body = (
237
+ block_body
238
+ if else_index is None
239
+ else block_body[:else_index]
240
+ )
241
+ else:
242
+ selected_body = (
243
+ []
244
+ if else_index is None
245
+ else block_body[else_index + 1:]
246
+ )
247
+
248
+ # Execute only the selected branch. A return exits
249
+ # this function immediately.
250
+ result = execute_function_lines(selected_body)
251
+
252
+ if result is not _NO_RETURN:
253
+ return result
254
+
186
255
  else:
187
- raise SyntaxError(f"Unsupported function block `{code}`")
256
+ raise SyntaxError(
257
+ f"Unsupported function block `{code}`"
258
+ )
259
+
188
260
  i = end + 1
189
261
  continue
190
262
 
191
263
  if line == "[endblock]":
192
- raise SyntaxError(f"Unexpected `[endblock]` in function `{name}`")
264
+ raise SyntaxError(
265
+ f"Unexpected `[endblock]` in function `{name}`"
266
+ )
267
+
268
+ # Handle return statements inside the function.
269
+ if re.match(r"^return\b", line):
270
+ expression = re.sub(r"^return\b", "", line, count=1).strip()
193
271
 
194
- if line.startswith("return"):
195
- expression = line[len("return"):].strip()
196
272
  if expression.startswith("[") and expression.endswith("]"):
197
- expression = expression[1:-1]
273
+ expression = expression[1:-1].strip()
274
+
198
275
  expression = substitute_parameters(expression)
276
+ expression = _expand_definitions(expression)
277
+
199
278
  return evaluate_python(expression) if expression else None
200
279
 
201
280
  normalized = normalize_let_line(line)
281
+
202
282
  if normalized != line:
203
- # The wrapper is ChemQL syntax; pass the assignment itself to Python.
204
283
  evaluate_python(normalized[1:-1])
284
+ elif line.startswith("[") and line.endswith("]"):
285
+ expression = line[1:-1].strip()
286
+ expression = substitute_parameters(expression)
287
+ expression = _expand_definitions(expression)
288
+ evaluate_python(expression)
205
289
  else:
206
- evaluate_python(substitute_parameters(line))
290
+ expression = substitute_parameters(line)
291
+ expression = _expand_definitions(expression)
292
+ evaluate_python(expression)
293
+
207
294
  i += 1
208
- return None
295
+
296
+ return _NO_RETURN
297
+
298
+ try:
299
+ result = execute_function_lines(definition["body"])
300
+ return None if result is _NO_RETURN else result
301
+
209
302
  finally:
210
- # Keep function-local assignments from leaking into the caller's namespace.
303
+ # Restore the caller's namespaces.
211
304
  python_globals.clear()
212
305
  python_globals.update(saved_globals)
306
+
213
307
  CHEMQL_VARIABLES.clear()
214
308
  CHEMQL_VARIABLES.update(saved_chemql_variables)
309
+
215
310
  if had_arguments:
216
311
  python_globals["arguments"] = saved_arguments
217
312
  else:
@@ -258,17 +353,15 @@ _shell_process = None
258
353
  _shell_master_fd = None
259
354
  _shell_prompt_ready = False
260
355
  _shell_output_streamed = False
356
+ _shell_output_queue = None
357
+ _shell_reader_thread = None
261
358
  _shell_prompt = f"__CHEMQL_PROMPT_{uuid.uuid4().hex}__"
262
359
 
263
360
 
264
361
  def get_shell():
265
- """Return the persistent shell process.
266
-
267
- On POSIX, use a pseudo-terminal so foreground commands such as `read`,
268
- `read -p`, and interactive CLIs can receive keyboard input. Windows keeps
269
- the pipe-based implementation until a ConPTY backend is added.
270
- """
362
+ """Return the persistent OS shell process for the current platform."""
271
363
  global _shell_process, _shell_master_fd, _shell_prompt_ready
364
+ global _shell_output_queue, _shell_reader_thread
272
365
 
273
366
  if _shell_process is not None and _shell_process.poll() is None:
274
367
  return _shell_process
@@ -277,14 +370,38 @@ def get_shell():
277
370
  close_shell()
278
371
 
279
372
  if os.name == "nt":
373
+ # Use PowerShell as ChemQL's native Windows shell. Its prompt function
374
+ # emits a private marker when a command finishes, so we do not append a
375
+ # marker to stdin (which would accidentally become Read-Host input).
376
+ setup = (
377
+ f"function global:prompt {{ [Console]::Out.WriteLine('{_shell_prompt}'); return '' }}"
378
+ )
280
379
  _shell_process = subprocess.Popen(
281
- ["cmd.exe", "/D", "/Q", "/K", "prompt $S"],
380
+ ["powershell.exe", "-NoLogo", "-NoProfile", "-NoExit", "-Command", setup],
282
381
  stdin=subprocess.PIPE,
283
382
  stdout=subprocess.PIPE,
284
383
  stderr=subprocess.STDOUT,
285
384
  text=True,
286
385
  bufsize=1,
287
386
  )
387
+ output_queue = queue.Queue()
388
+ _shell_output_queue = output_queue
389
+ process = _shell_process
390
+
391
+ def read_output():
392
+ try:
393
+ while True:
394
+ character = process.stdout.read(1) # type: ignore
395
+ if not character:
396
+ break
397
+ output_queue.put(character)
398
+ except (OSError, ValueError):
399
+ pass
400
+ finally:
401
+ output_queue.put(None)
402
+
403
+ _shell_reader_thread = threading.Thread(target=read_output, daemon=True)
404
+ _shell_reader_thread.start()
288
405
  else:
289
406
  import fcntl
290
407
  import pty
@@ -434,47 +551,128 @@ def _run_shell_command_posix(command, marker=None):
434
551
  return f"Shell error: {exc}"
435
552
 
436
553
 
437
- def run_shell_command(command):
554
+ def _run_shell_command_windows(command):
555
+ """Run a command in persistent PowerShell and forward line input interactively."""
556
+ global _shell_prompt_ready, _shell_output_streamed
438
557
  shell = get_shell()
439
- command = command.replace("\r\n", "\n").replace("\r", "\n").rstrip("\n")
440
- marker = f"__CHEMQL_END_{uuid.uuid4().hex}__"
558
+ output_queue = _shell_output_queue
559
+ if output_queue is None:
560
+ return "Shell error: PowerShell output reader was not initialized."
441
561
 
442
- try:
443
- if os.name != "nt":
444
- return _run_shell_command_posix(command, marker)
562
+ import msvcrt
563
+
564
+ def wait_for_prompt(stream_output):
565
+ collected = ""
566
+ streamed_pos = 0
567
+ marker = _shell_prompt
568
+ marker_len = len(marker)
569
+ stdin_is_tty = False
570
+ try:
571
+ stdin_is_tty = sys.stdin is not None and sys.stdin.isatty()
572
+ except (AttributeError, OSError, ValueError):
573
+ pass
574
+ pending_input = ""
445
575
 
446
- # Windows pipe mode supports persistent commands, but not fully
447
- # interactive programs yet; a ConPTY backend is needed for that.
448
- shell.stdin.write(command + "\r\n" + f"echo {marker}\r\n") # type: ignore
449
- shell.stdin.flush() # type: ignore
450
- output = []
451
576
  while True:
577
+ # While a shell command is running, pass complete lines typed by the
578
+ # user to PowerShell. This lets Read-Host receive real input rather
579
+ # than consuming a completion marker injected by ChemQL.
580
+ if stdin_is_tty:
581
+ try:
582
+ while msvcrt.kbhit(): #type:ignore
583
+ key = msvcrt.getwch() #type:ignore
584
+ if key in ("\x00", "\xe0"): #type:ignore
585
+ # Consume the second half of a Windows special key.
586
+ if msvcrt.kbhit(): #type:ignore
587
+ msvcrt.getwch() #type:ignore
588
+ elif key in ("\r", "\n"):
589
+ shell.stdin.write(pending_input + "\r\n") # type: ignore
590
+ shell.stdin.flush() # type: ignore
591
+ pending_input = ""
592
+ elif key == "\b":
593
+ pending_input = pending_input[:-1]
594
+ 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
+ close_shell()
598
+ return None
599
+ elif key >= " " and key != "\x7f":
600
+ pending_input += key
601
+ except (OSError, ValueError):
602
+ stdin_is_tty = False
603
+
452
604
  try:
453
- line = shell.stdout.readline() # type: ignore
454
- except KeyboardInterrupt:
455
- # Avoid leaking a traceback; reset the shell so the next command
456
- # starts with a clean process if the current one was interrupted.
457
- close_shell()
458
- return "Shell command interrupted. Shell session was reset."
459
- if not line:
460
- return "\n".join(output) or "Shell session terminated."
461
- line = line.rstrip("\r\n")
462
- if marker in line:
463
- break
464
- output.append(line)
465
- return "\n".join(output).strip()
605
+ character = output_queue.get(timeout=0.04)
606
+ except queue.Empty:
607
+ continue
608
+ if character is None:
609
+ return None
466
610
 
611
+ collected += character
612
+ marker_pos = collected.find(marker)
613
+ if marker_pos >= 0:
614
+ visible = collected[:marker_pos]
615
+ if stream_output and marker_pos > streamed_pos:
616
+ sys.stdout.write(collected[streamed_pos:marker_pos])
617
+ sys.stdout.flush()
618
+ _shell_output_streamed = True
619
+ return visible.replace("\r\n", "\n").rstrip("\r\n ")
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
636
+
637
+ if not _shell_prompt_ready:
638
+ initial = wait_for_prompt(stream_output=False)
639
+ if initial is None:
640
+ close_shell()
641
+ return "Shell session terminated."
642
+ _shell_prompt_ready = True
643
+
644
+ try:
645
+ shell.stdin.write(command.replace("\n", "\r\n") + "\r\n") # type: ignore
646
+ shell.stdin.flush() # type: ignore
647
+ result = wait_for_prompt(stream_output=True)
648
+ if result is None:
649
+ close_shell()
650
+ return "Shell session terminated."
651
+ return result
652
+ except (BrokenPipeError, OSError, ValueError) as exc:
653
+ close_shell()
654
+ return f"Shell error: {exc}"
655
+
656
+
657
+ def run_shell_command(command):
658
+ global _shell_output_streamed
659
+ command = command.replace("\r\n", "\n").replace("\r", "\n").rstrip("\n")
660
+ try:
661
+ if os.name == "nt":
662
+ if command.strip().casefold() in {"powershell", "powershell.exe", "pwsh", "pwsh.exe"}:
663
+ return "ChemQL already uses PowerShell on Windows; run the command directly instead of starting a nested shell."
664
+ _shell_output_streamed = False
665
+ return _run_shell_command_windows(command)
666
+ return _run_shell_command_posix(command)
467
667
  except KeyboardInterrupt:
468
668
  close_shell()
469
669
  return "Shell command interrupted. Shell session was reset."
470
- except (BrokenPipeError, OSError) as exc:
471
- close_shell()
472
- return f"Shell error: {exc}"
473
670
 
474
671
 
475
672
  def close_shell():
476
673
  """Clean up the persistent shell and its pseudo-terminal on exit."""
477
674
  global _shell_process, _shell_master_fd, _shell_prompt_ready
675
+ global _shell_output_queue, _shell_reader_thread
478
676
 
479
677
  if _shell_process is not None:
480
678
  if _shell_process.poll() is None:
@@ -496,6 +694,8 @@ def close_shell():
496
694
  pass
497
695
  _shell_master_fd = None
498
696
  _shell_prompt_ready = False
697
+ _shell_output_queue = None
698
+ _shell_reader_thread = None
499
699
 
500
700
 
501
701
  atexit.register(close_shell)
@@ -2486,7 +2686,7 @@ def execute_if_block(header, body):
2486
2686
  )
2487
2687
 
2488
2688
  for section_header, section_body in sections:
2489
- code = section_header[1:-1].strip()
2689
+ code = _expand_definitions(section_header[1:-1].strip())
2490
2690
 
2491
2691
  if code.startswith("if "):
2492
2692
  condition = code[3:].strip()
@@ -3229,7 +3429,7 @@ def execute(arguments, query_text=None, positions=None):
3229
3429
 
3230
3430
  if arguments[0].lower() == "macros":
3231
3431
  return "\n".join([f"@{name} = {value}" for name, value in MACROS.items()]) if MACROS else "No macros."
3232
-
3432
+
3233
3433
  if arguments[0] == "conditions":
3234
3434
  if len(arguments) != 1:
3235
3435
  return "Usage: conditions"
@@ -3584,7 +3784,7 @@ class ChemqlLexer(Lexer):
3584
3784
  "findmol", "findre", "set", "add", "remove", "react",
3585
3785
  "balance", "conditions", "view", "help", "dump", "version",
3586
3786
  "return", "union", "intersection", "let", "unset", "variables",
3587
- "macros", "definitions", "arguments"
3787
+ "macros", "definitions", "arguments",
3588
3788
  }
3589
3789
 
3590
3790
  OPERATORS = {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chemql-python
3
- Version: 1.2.1
3
+ Version: 1.2.3
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.0 combines database-style
20
+ elements, molecules, and reactions. Version 1.2.3 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.0\
28
- > The examples below describe the ChemQL 1.2.0 source. Dataset
27
+ > **Version covered by this README:** 1.2.3\
28
+ > The examples below describe the ChemQL 1.2.3 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.0 adds three ways to reuse values and logic. These declarations
541
+ ChemQL 1.2.3 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.0
836
+ chemql --upgrade 1.2.3
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=7YotC6BZV7ZRPQjQwFfmOmlbri9tdKPloQ40Lm_nRZg,152580
1
+ chemql/__init__.py,sha256=buPnjHRRx32NB8I6hXUgKoKLzy3R_FkdNYqm1mmptLA,159524
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.1.dist-info/licenses/LICENSE,sha256=o4ywHC0EwRy2zgIASiRD2sK3Vcsf7LBcTHjDvoJgRE0,1079
12
- chemql_python-1.2.1.dist-info/METADATA,sha256=iKKC3niSxAHMvcc0CxxgfeDw9EhQDx0RZXt7_kdbDck,30529
13
- chemql_python-1.2.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
14
- chemql_python-1.2.1.dist-info/entry_points.txt,sha256=j98ObVWR743an524z24AiWeRsppag-py5cc7l0D0HkI,39
15
- chemql_python-1.2.1.dist-info/top_level.txt,sha256=9UzHwnk_F6W19Q9nayaFgVyAELQXXwoYjeBgB6MWNr8,7
16
- chemql_python-1.2.1.dist-info/RECORD,,
11
+ chemql_python-1.2.3.dist-info/licenses/LICENSE,sha256=o4ywHC0EwRy2zgIASiRD2sK3Vcsf7LBcTHjDvoJgRE0,1079
12
+ chemql_python-1.2.3.dist-info/METADATA,sha256=HMq0gKLpGQrzdy9Ycj5evg8E0D57_Hfr28TSNOQHuXM,30529
13
+ chemql_python-1.2.3.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
14
+ chemql_python-1.2.3.dist-info/entry_points.txt,sha256=j98ObVWR743an524z24AiWeRsppag-py5cc7l0D0HkI,39
15
+ chemql_python-1.2.3.dist-info/top_level.txt,sha256=9UzHwnk_F6W19Q9nayaFgVyAELQXXwoYjeBgB6MWNr8,7
16
+ chemql_python-1.2.3.dist-info/RECORD,,