QuLab 2.8.0__cp310-cp310-win_amd64.whl → 2.9.0__cp310-cp310-win_amd64.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.
qulab/__init__.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from .executor.storage import find_report
2
2
  from .executor.storage import get_report_by_index as get_report
3
+ from .executor.template import VAR
3
4
  from .executor.utils import debug_analyze
4
5
  from .scan import Scan, get_record, load_record, lookup, lookup_list
5
6
  from .version import __version__
qulab/executor/storage.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import hashlib
2
2
  import lzma
3
3
  import pickle
4
+ import re
4
5
  import uuid
5
6
  import zipfile
6
7
  from dataclasses import dataclass, field
@@ -8,6 +9,7 @@ from datetime import datetime, timedelta
8
9
  from functools import lru_cache
9
10
  from pathlib import Path
10
11
  from typing import Any, Literal
12
+ from urllib.parse import parse_qs
11
13
 
12
14
  from loguru import logger
13
15
 
@@ -15,10 +17,15 @@ try:
15
17
  from paramiko import SSHClient
16
18
  from paramiko.ssh_exception import SSHException
17
19
  except:
20
+ import warnings
21
+
22
+ warnings.warn("Can't import paramiko, ssh support will be disabled.")
23
+
18
24
  class SSHClient:
19
25
 
20
26
  def __init__(self):
21
- raise ImportError("Can't import paramiko, ssh support will be disabled.")
27
+ raise ImportError(
28
+ "Can't import paramiko, ssh support will be disabled.")
22
29
 
23
30
  def __enter__(self):
24
31
  return self
@@ -26,6 +33,9 @@ except:
26
33
  def __exit__(self, exc_type, exc_value, traceback):
27
34
  pass
28
35
 
36
+ class SSHException(Exception):
37
+ pass
38
+
29
39
 
30
40
  from ..cli.config import get_config_value
31
41
 
@@ -48,7 +58,7 @@ class Report():
48
58
  index: int = -1
49
59
  previous_path: Path | None = field(default=None, repr=False)
50
60
  heads: dict[str, Path] = field(default_factory=dict, repr=False)
51
- base_path: Path | None = field(default=None, repr=False)
61
+ base_path: str | Path | None = field(default=None, repr=False)
52
62
  path: Path | None = field(default=None, repr=False)
53
63
  config_path: Path | None = field(default=None, repr=False)
54
64
  script_path: Path | None = field(default=None, repr=False)
@@ -335,12 +345,15 @@ def save_config_key_history(key: str, report: Report,
335
345
 
336
346
 
337
347
  def load_report(path: str | Path, base_path: str | Path) -> Report | None:
338
- if isinstance(base_path, str) and base_path.startswith('ssh '):
348
+ if isinstance(base_path, str) and base_path.startswith('ssh://'):
339
349
  with SSHClient() as client:
340
- cfg, base_path = _pase_ssh_config(base_path[4:])
350
+ cfg = parse_ssh_uri(base_path)
351
+ remote_base_path = cfg.pop('remote_file_path')
341
352
  client.load_system_host_keys()
342
353
  client.connect(**cfg)
343
- return load_report_from_scp(path, base_path, client)
354
+ report = load_report_from_scp(path, remote_base_path, client)
355
+ report.base_path = base_path
356
+ return report
344
357
 
345
358
  base_path = Path(base_path)
346
359
  if zipfile.is_zipfile(base_path):
@@ -357,12 +370,13 @@ def load_report(path: str | Path, base_path: str | Path) -> Report | None:
357
370
 
358
371
 
359
372
  def get_heads(base_path: str | Path) -> Path | None:
360
- if isinstance(base_path, str) and base_path.startswith('ssh '):
373
+ if isinstance(base_path, str) and base_path.startswith('ssh://'):
361
374
  with SSHClient() as client:
362
- cfg, base_path = _pase_ssh_config(base_path[4:])
375
+ cfg = parse_ssh_uri(base_path)
376
+ remote_base_path = cfg.pop('remote_file_path')
363
377
  client.load_system_host_keys()
364
378
  client.connect(**cfg)
365
- return get_heads_from_scp(base_path, client)
379
+ return get_heads_from_scp(remote_base_path, client)
366
380
 
367
381
  base_path = Path(base_path)
368
382
  if zipfile.is_zipfile(base_path):
@@ -377,12 +391,13 @@ def get_heads(base_path: str | Path) -> Path | None:
377
391
 
378
392
  @lru_cache(maxsize=4096)
379
393
  def query_index(name: str, base_path: str | Path, index: int):
380
- if isinstance(base_path, str) and base_path.startswith('ssh '):
394
+ if isinstance(base_path, str) and base_path.startswith('ssh://'):
381
395
  with SSHClient() as client:
382
- cfg, base_path = _pase_ssh_config(base_path[4:])
396
+ cfg = parse_ssh_uri(base_path)
397
+ remote_base_path = cfg.pop('remote_file_path')
383
398
  client.load_system_host_keys()
384
399
  client.connect(**cfg)
385
- return query_index_from_scp(name, base_path, client, index)
400
+ return query_index_from_scp(name, remote_base_path, client, index)
386
401
 
387
402
  base_path = Path(base_path)
388
403
  if zipfile.is_zipfile(base_path):
@@ -398,12 +413,13 @@ def query_index(name: str, base_path: str | Path, index: int):
398
413
 
399
414
  @lru_cache(maxsize=4096)
400
415
  def load_item(id, base_path):
401
- if isinstance(base_path, str) and base_path.startswith('ssh '):
416
+ if isinstance(base_path, str) and base_path.startswith('ssh://'):
402
417
  with SSHClient() as client:
403
- cfg, base_path = _pase_ssh_config(base_path[4:])
418
+ cfg = parse_ssh_uri(base_path)
419
+ remote_base_path = cfg.pop('remote_file_path')
404
420
  client.load_system_host_keys()
405
421
  client.connect(**cfg)
406
- buf = load_item_buf_from_scp(id, base_path, client)
422
+ buf = load_item_buf_from_scp(id, remote_base_path, client)
407
423
  else:
408
424
  base_path = Path(base_path)
409
425
  if zipfile.is_zipfile(base_path):
@@ -489,15 +505,56 @@ def load_item_buf_from_zipfile(id, base_path):
489
505
  #########################################################################
490
506
 
491
507
 
492
- def _pase_ssh_config(config: str):
493
- config = config.split()
494
- base_path = ' '.join(config[4:])
508
+ def parse_ssh_uri(uri):
509
+ """
510
+ 解析 SSH URI 字符串,返回包含连接参数和路径的字典。
511
+
512
+ 格式:ssh://[{username}[:{password}]@]{host}[:{port}][?key_filename={key_path}][/{remote_file_path}]
513
+
514
+ 返回示例:
515
+ {
516
+ "username": "user",
517
+ "password": "pass",
518
+ "host": "example.com",
519
+ "port": 22,
520
+ "key_filename": "/path/to/key",
521
+ "remote_file_path": "/data/file.txt"
522
+ }
523
+ """
524
+ pattern = re.compile(
525
+ r"^ssh://" # 协议头
526
+ r"(?:([^:@/]+))(?::([^@/]+))?@?" # 用户名和密码(可选)
527
+ r"([^:/?#]+)" # 主机名(必须)
528
+ r"(?::(\d+))?" # 端口(可选)
529
+ r"(/?[^?#]*)?" # 远程路径(可选)
530
+ r"(?:\?([^#]+))?" # 查询参数(如 key_filename)
531
+ r"$",
532
+ re.IGNORECASE)
533
+
534
+ match = pattern.match(uri)
535
+ if not match:
536
+ raise ValueError(f"Invalid SSH URI format: {uri}")
537
+
538
+ # 提取分组
539
+ username, password, host, port, path, query = match.groups()
540
+
541
+ # 处理查询参数
542
+ key_filename = None
543
+ if query:
544
+ params = parse_qs(query)
545
+ key_filename = params.get("key_filename", [None])[0] # 取第一个值
546
+
547
+ # 清理路径开头的斜杠
548
+ remote_file_path = path
549
+
495
550
  return {
496
- 'hostname': config[0],
497
- 'port': int(config[1]),
498
- 'username': config[2],
499
- 'key_filename': config[3]
500
- }, Path(base_path)
551
+ "username": username,
552
+ "password": password,
553
+ "hostname": host,
554
+ "port": int(port) if port else 22, # 默认端口 22
555
+ "key_filename": key_filename,
556
+ "remote_file_path": remote_file_path
557
+ }
501
558
 
502
559
 
503
560
  def load_report_from_scp(path: str | Path, base_path: Path,
@@ -508,7 +565,6 @@ def load_report_from_scp(path: str | Path, base_path: Path,
508
565
  with sftp.open(str(Path(base_path) / 'reports' / path), 'rb') as f:
509
566
  index = int.from_bytes(f.read(8), 'big')
510
567
  report = pickle.loads(lzma.decompress(f.read()))
511
- report.base_path = path
512
568
  report.index = index
513
569
  return report
514
570
  except SSHException:
@@ -532,7 +588,7 @@ def query_index_from_scp(name: str, base_path: Path, client: SSHClient,
532
588
  s = str(Path(base_path) / 'index' / f'{name}.width')
533
589
  with sftp.open(s, 'rb') as f:
534
590
  width = int(f.read().decode())
535
- with sftp.open(str(base_path / 'index' / f'{name}.idx'),
591
+ with sftp.open(str(Path(base_path) / 'index' / f'{name}.idx'),
536
592
  'rb') as f:
537
593
  f.seek(index * (width + 1))
538
594
  context = f.read(width).decode()
@@ -8,6 +8,12 @@ import string
8
8
  import textwrap
9
9
  from typing import Any
10
10
 
11
+ _notset = object()
12
+
13
+
14
+ def VAR(name: str, /, *, default: Any = _notset) -> Any:
15
+ return name
16
+
11
17
 
12
18
  def encode_mapping(mapping):
13
19
  mapping_bytes = lzma.compress(pickle.dumps(mapping))
@@ -68,26 +74,67 @@ class TemplateVarExtractor(ast.NodeVisitor):
68
74
  def visit_Call(self, node):
69
75
  if isinstance(node.func, ast.Name) and node.func.id == 'VAR':
70
76
  arg = node.args[0]
71
- if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
72
- if arg.value not in self.mapping:
73
- raise TemplateKeyError(
74
- f"The variable '{arg.value}' is not provided in mapping. {self.fname}:{node.lineno}"
77
+ if not isinstance(arg, ast.Constant) or not isinstance(
78
+ arg.value, str):
79
+ raise SyntaxError(
80
+ f"Argument of VAR function must be a string. {self.fname}:{node.lineno}"
81
+ )
82
+ if len(node.args) != 1:
83
+ raise SyntaxError(
84
+ f"VAR function only accept one argument. {self.fname}:{node.lineno}"
85
+ )
86
+ default = _notset
87
+ for k in node.keywords:
88
+ if k.arg == 'default':
89
+ pass
90
+ # if isinstance(k.value, ast.Constant):
91
+ # # default = k.value.value
92
+ # default = k.value
93
+ # else:
94
+ # default = k.value
95
+ # # raise SyntaxError(
96
+ # # f"Argument of 'default' must be a constant. {self.fname}:{node.lineno}"
97
+ # # )
98
+ else:
99
+ raise SyntaxError(
100
+ f"VAR function only accept keyword argument 'default'. {self.fname}:{node.lineno}"
75
101
  )
76
- self.variables.add(arg.value)
102
+
103
+ if default is _notset:
77
104
  # new_node = ast.Subscript(value=ast.Name(id="__VAR",
78
105
  # ctx=ast.Load()),
79
106
  # slice=ast.Constant(value=arg.value),
80
107
  # ctx=ast.Load())
81
- # ast.fix_missing_locations(new_node)
82
- # new_source = ast.unparse(new_node)
108
+ if arg.value not in self.mapping:
109
+ raise TemplateKeyError(
110
+ f"The variable '{arg.value}' is not provided in mapping. {self.fname}:{node.lineno}"
111
+ )
83
112
  self.replacements[(node.lineno, node.end_lineno,
84
113
  node.col_offset,
85
114
  node.end_col_offset)] = ('VAR', arg.value,
86
115
  None, None)
87
116
  else:
88
- raise SyntaxError(
89
- f"Argument of VAR function must be a string. {self.fname}:{node.lineno}"
90
- )
117
+ # new_node = ast.Call(
118
+ # func=ast.Attribute(value=ast.Name(id='__VAR',
119
+ # ctx=ast.Load()),
120
+ # attr='get',
121
+ # ctx=ast.Load()),
122
+ # args=[ast.Constant(value=arg.value)],
123
+ # keywords=[
124
+ # ast.keyword(arg='default',
125
+ # value=value)
126
+ # ],
127
+ # ctx=ast.Load())
128
+ self.replacements[(node.lineno, node.end_lineno,
129
+ node.col_offset,
130
+ node.end_col_offset)] = ('VAR', arg.value,
131
+ None, None)
132
+ # ast.fix_missing_locations(new_node)
133
+ # new_source = ast.unparse(new_node)
134
+ # print(new_source)
135
+
136
+ self.variables.add(arg.value)
137
+
91
138
  self.generic_visit(node)
92
139
 
93
140
  def _process_string(self, s: str, lineno: int, col_offset: int,
@@ -160,8 +207,24 @@ def inject_mapping(source: str, mapping: dict[str, Any],
160
207
  lines_offset[end_lineno - 1] += len(
161
208
  lines[end_lineno - 1]) - length_of_last_line
162
209
  else:
163
- pattern = re.compile(r'VAR\s*\(\s*(["\'])(\w+)\1\s*\)')
164
- replacement = f'__VAR_{hash_str}' + r'[\1\2\1]'
210
+ pattern = re.compile(
211
+ r'VAR\s*\(\s*' # VAR(
212
+ r'(["\'])(\w+)\1' # 第一个参数(引号包裹的变量名)
213
+ r'(?:\s*,\s*(.*?))?' # 可选的其他参数
214
+ r'\s*\)', # 闭合括号
215
+ re.DOTALL # 允许.匹配换行符
216
+ ) # yapf: disable
217
+
218
+ def replacement(match):
219
+ quote = match.group(1)
220
+ var_name = match.group(2)
221
+ extra_args = match.group(3)
222
+ base = f'__VAR_{hash_str}'
223
+ if extra_args is not None:
224
+ return f'{base}.get({quote}{var_name}{quote}, {extra_args})'
225
+ else:
226
+ return f'{base}[{quote}{var_name}{quote}]'
227
+
165
228
  new_content = re.sub(pattern, replacement, content)
166
229
 
167
230
  if lineno == end_lineno:
@@ -172,7 +235,7 @@ def inject_mapping(source: str, mapping: dict[str, Any],
172
235
  lines[lineno - 1] = head + new_content[:-1]
173
236
  for i in range(lineno, end_lineno - 1):
174
237
  lines[i] = ''
175
- lines[end_lineno - 1] = ']' + tail
238
+ lines[end_lineno - 1] = new_content[-1] + tail
176
239
  lines_offset[end_lineno - 1] += len(
177
240
  lines[end_lineno - 1]) - length_of_last_line
178
241
 
Binary file
qulab/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "2.8.0"
1
+ __version__ = "2.9.0"
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: QuLab
3
- Version: 2.8.0
3
+ Version: 2.9.0
4
4
  Summary: contral instruments and manage data
5
5
  Author-email: feihoo87 <feihoo87@gmail.com>
6
6
  Maintainer-email: feihoo87 <feihoo87@gmail.com>
@@ -44,6 +44,7 @@ Requires-Dist: watchdog>=4.0.0
44
44
  Requires-Dist: waveforms>=1.9.4
45
45
  Provides-Extra: full
46
46
  Requires-Dist: uvloop>=0.19.0; extra == "full"
47
+ Dynamic: license-file
47
48
 
48
49
  # QuLab
49
50
  [![View build status](https://travis-ci.org/feihoo87/QuLab.svg?branch=master)](https://travis-ci.org/feihoo87/QuLab)
@@ -1,10 +1,10 @@
1
- qulab/__init__.py,sha256=RZme5maBSMZpP6ckXymqZpo2sRYttwEpTYCIzIvys1c,292
1
+ qulab/__init__.py,sha256=RrWRvG8Lw27zMr7XP8s-z43b09-wiwPi0ZtiNWbky-c,328
2
2
  qulab/__main__.py,sha256=FL4YsGZL1jEtmcPc5WbleArzhOHLMsWl7OH3O-1d1ss,72
3
3
  qulab/dicttree.py,sha256=ZoSJVWK4VMqfzj42gPb_n5RqLlM6K1Me0WmLIfLEYf8,14195
4
- qulab/fun.cp310-win_amd64.pyd,sha256=IFR7znN7dCxfNQ3OHZgTsi1G8b8cSlyLacwV_oGBWnI,31744
4
+ qulab/fun.cp310-win_amd64.pyd,sha256=63fEHjls7G98kQqq6YbxcB-nLL4DNhUV4U_oDAWjNUM,31744
5
5
  qulab/typing.py,sha256=PRtwbCHWY2ROKK8GHq4Bo8llXrIGo6xC73DrQf7S9os,71
6
6
  qulab/utils.py,sha256=65N2Xj7kqRsQ4epoLNY6tL-i5ts6Wk8YuJYee3Te6zI,3077
7
- qulab/version.py,sha256=HoslYmXXy3WAQMEXd5jNDwMD2rL7bVPI5DzutSN5GNc,21
7
+ qulab/version.py,sha256=a31Mxt9ZbwomFaywnjMS--InOBZRSD7ltY06SX9P5q0,21
8
8
  qulab/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  qulab/cli/commands.py,sha256=6xd2eYw32k1NmfAuYSu__1kaP12Oz1QVqwbkYXdWno4,588
10
10
  qulab/cli/config.py,sha256=7h3k0K8FYHhI6LVWt8BoDdKrX2ApFDBAUAUuXhHwst4,3799
@@ -12,8 +12,8 @@ qulab/executor/__init__.py,sha256=LosPzOMaljSZY1thy_Fxtbrgq7uubJszMABEB7oM7tU,10
12
12
  qulab/executor/cli.py,sha256=8d-8bRWZ5lmsMtjASsl1zu1rV-syeAESMNVthvIQxlo,10018
13
13
  qulab/executor/load.py,sha256=YndvzagvWR8Sg6WHZ-gP-Of0FrFOyh_E_a3VXsjDf1Q,17502
14
14
  qulab/executor/schedule.py,sha256=0BV5LGxhqdIlGwW6-o5_5mljAtdtL1La8EDNBFi8pzU,18585
15
- qulab/executor/storage.py,sha256=2YWUxYis8QlYLhzvvwoS2Wmyb9UYaojEh6X20ICePWI,19014
16
- qulab/executor/template.py,sha256=bKMoOBPfa3XMgTfGHQK6pDTswH1vcIjnopaWE3UKpP0,7726
15
+ qulab/executor/storage.py,sha256=OA_XMDoFDfPZCU89caf9-VZ3D6qaWib8MpJno10KUfc,20770
16
+ qulab/executor/template.py,sha256=KOSOC-MTcHHCudtBVgZwNpK5dstTz_os7bcU1Yx0mAs,10372
17
17
  qulab/executor/transform.py,sha256=BDx0c4nqTHMAOLVqju0Ydd91uxNm6EpVIfssjZse0bI,2284
18
18
  qulab/executor/utils.py,sha256=l_b0y2kMwYKyyXeFtoblPYwKNU-wiFQ9PMo9QlWl9wE,6213
19
19
  qulab/monitor/__init__.py,sha256=xEVDkJF8issrsDeLqQmDsvtRmrf-UiViFcGTWuzdlFU,43
@@ -97,9 +97,9 @@ qulab/visualization/plot_seq.py,sha256=Uo1-dB1YE9IN_A9tuaOs9ZG3S5dKDQ_l98iD2Wbxp
97
97
  qulab/visualization/qdat.py,sha256=HubXFu4nfcA7iUzghJGle1C86G6221hicLR0b-GqhKQ,5887
98
98
  qulab/visualization/rot3d.py,sha256=jGHJcqj1lEWBUV-W4GUGONGacqjrYvuFoFCwPse5h1Y,757
99
99
  qulab/visualization/widgets.py,sha256=HcYwdhDtLreJiYaZuN3LfofjJmZcLwjMfP5aasebgDo,3266
100
- qulab-2.8.0.dist-info/LICENSE,sha256=b4NRQ-GFVpJMT7RuExW3NwhfbrYsX7AcdB7Gudok-fs,1086
101
- qulab-2.8.0.dist-info/METADATA,sha256=7GHqBOgAMQ_zxJaGPbsc6AbwGMQ7IObgjC9Dh5k7ggM,3803
102
- qulab-2.8.0.dist-info/WHEEL,sha256=H72wNgFePEN0L06A2Z11ydRFbMa6Lsr93VFntInNpxE,101
103
- qulab-2.8.0.dist-info/entry_points.txt,sha256=b0v1GXOwmxY-nCCsPN_rHZZvY9CtTbWqrGj8u1m8yHo,45
104
- qulab-2.8.0.dist-info/top_level.txt,sha256=3T886LbAsbvjonu_TDdmgxKYUn939BVTRPxPl9r4cEg,6
105
- qulab-2.8.0.dist-info/RECORD,,
100
+ qulab-2.9.0.dist-info/licenses/LICENSE,sha256=b4NRQ-GFVpJMT7RuExW3NwhfbrYsX7AcdB7Gudok-fs,1086
101
+ qulab-2.9.0.dist-info/METADATA,sha256=5OizsNx2VbjgbNg5S1euXqlf9CrmsKbGW1oSoNFkVrs,3826
102
+ qulab-2.9.0.dist-info/WHEEL,sha256=3P80pz7aU84nVq-51hAoVw99hqBBuyZ6hPufE-F7jcM,101
103
+ qulab-2.9.0.dist-info/entry_points.txt,sha256=b0v1GXOwmxY-nCCsPN_rHZZvY9CtTbWqrGj8u1m8yHo,45
104
+ qulab-2.9.0.dist-info/top_level.txt,sha256=3T886LbAsbvjonu_TDdmgxKYUn939BVTRPxPl9r4cEg,6
105
+ qulab-2.9.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (76.1.0)
2
+ Generator: setuptools (77.0.3)
3
3
  Root-Is-Purelib: false
4
4
  Tag: cp310-cp310-win_amd64
5
5