QuLab 2.3.6__cp310-cp310-macosx_10_9_universal2.whl → 2.4.1__cp310-cp310-macosx_10_9_universal2.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.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: QuLab
3
- Version: 2.3.6
3
+ Version: 2.4.1
4
4
  Summary: contral instruments and manage data
5
5
  Author-email: feihoo87 <feihoo87@gmail.com>
6
6
  Maintainer-email: feihoo87 <feihoo87@gmail.com>
@@ -24,23 +24,26 @@ Classifier: Programming Language :: Python :: 3.12
24
24
  Requires-Python: >=3.10
25
25
  Description-Content-Type: text/markdown
26
26
  License-File: LICENSE
27
- Requires-Dist: blinker >=1.4
28
- Requires-Dist: click >=7.1.2
29
- Requires-Dist: dill >=0.3.6
30
- Requires-Dist: GitPython >=3.1.14
31
- Requires-Dist: ipython >=7.4.0
32
- Requires-Dist: ipywidgets >=7.4.2
33
- Requires-Dist: loguru >=0.7.2
34
- Requires-Dist: matplotlib >=3.7.2
35
- Requires-Dist: nevergrad >=1.0.2
36
- Requires-Dist: numpy >=1.13.3
37
- Requires-Dist: ply >=3.11
38
- Requires-Dist: pyzmq >=25.1.0
39
- Requires-Dist: scipy >=1.0.0
40
- Requires-Dist: watchdog >=4.0.0
27
+ Requires-Dist: blinker>=1.4
28
+ Requires-Dist: click>=7.1.2
29
+ Requires-Dist: dill>=0.3.6
30
+ Requires-Dist: GitPython>=3.1.14
31
+ Requires-Dist: ipython>=7.4.0
32
+ Requires-Dist: ipywidgets>=7.4.2
33
+ Requires-Dist: loguru>=0.7.2
34
+ Requires-Dist: matplotlib>=3.7.2
35
+ Requires-Dist: msgpack>=1.0.5
36
+ Requires-Dist: nevergrad>=1.0.2
37
+ Requires-Dist: numpy>=1.13.3
38
+ Requires-Dist: ply>=3.11
39
+ Requires-Dist: pyzmq>=25.1.0
40
+ Requires-Dist: scipy>=1.0.0
41
+ Requires-Dist: scikit-optimize>=0.9.0
42
+ Requires-Dist: SQLAlchemy>=2.0.19
43
+ Requires-Dist: watchdog>=4.0.0
44
+ Requires-Dist: waveforms>=1.9.4
41
45
  Provides-Extra: full
42
- Requires-Dist: SQLAlchemy >=2.0.19 ; extra == 'full'
43
- Requires-Dist: uvloop >=0.19.0 ; extra == 'full'
46
+ Requires-Dist: uvloop>=0.19.0; extra == "full"
44
47
 
45
48
  # QuLab
46
49
  [![View build status](https://travis-ci.org/feihoo87/QuLab.svg?branch=master)](https://travis-ci.org/feihoo87/QuLab)
@@ -1,7 +1,15 @@
1
1
  qulab/__init__.py,sha256=P-Mx2p4TVmL91SoxoeXcj8Qm0x4xUf5Q_FLk0Yc_gIQ,138
2
- qulab/__main__.py,sha256=ZC1NKaoxKyy60DaCfB8vYnB1z3RXQ2j8E1sRZ4A8sXE,428
3
- qulab/fun.cpython-310-darwin.so,sha256=XvwXGsOfn3ONbwMDYo4dcO4KOOTMBORrvTOYoCz0I3o,159632
4
- qulab/version.py,sha256=ZYW5EwlmyblvQtaORgk_8_vizsHXlkdmJ__EGWdTMAY,21
2
+ qulab/__main__.py,sha256=V5jIyfuCSi5dI6jvqgH_BpXG0URd6bgneDdVCM1dGSA,545
3
+ qulab/dicttree.py,sha256=tRRMpGZYVOLw0TEByE3_2Ss8FdOmzuGL9e1DWbs8qoY,13684
4
+ qulab/fun.cpython-310-darwin.so,sha256=D8D2k4BdfCbo-CLgm27GMTgx-snj3Qtrc-xmkmDm-uc,126864
5
+ qulab/version.py,sha256=CqVHPUFnrZpBglKOmFBRr4nAIcRkzVmVVYwtVbAYLgM,21
6
+ qulab/executor/__init__.py,sha256=LosPzOMaljSZY1thy_Fxtbrgq7uubJszMABEB7oM7tU,101
7
+ qulab/executor/__main__.py,sha256=r4hRH0i4C83xcB_BVXXNNSnUkV-cPx2pGHj9E8h3l8M,2282
8
+ qulab/executor/load.py,sha256=jZHkEzFF8ufFAIajkRDKmHYYJxbxI3RcEbAFIbl9VQ0,6208
9
+ qulab/executor/schedule.py,sha256=u3S8b6AwfdXyM_kAfrWr9zCi15qS40nRRdFEVyzJMmI,8503
10
+ qulab/executor/storage.py,sha256=uYT2GtNEVX-lwICvDFBRH4hanBnPGue1lu2srvqYI_A,4518
11
+ qulab/executor/transform.py,sha256=Sy1ZmzkaP4RhUMempYxcIRjuzTxbC9UikUMBKuoVleE,2148
12
+ qulab/executor/utils.py,sha256=vrwk2fAfSp3YLVZfrYPvxsVPpeuE2n-4G2Ae0eqdT4s,3080
5
13
  qulab/monitor/__init__.py,sha256=nTHelnDpxRS_fl_B38TsN0njgq8eVTEz9IAnN3NbDlM,42
6
14
  qulab/monitor/__main__.py,sha256=w3yUcqq195LzSnXTkQcuC1RSFRhy4oQ_PEBmucXguME,97
7
15
  qulab/monitor/config.py,sha256=fQ5JcsMApKc1UwANEnIvbDQZl8uYW0tle92SaYtX9lI,744
@@ -13,14 +21,14 @@ qulab/monitor/ploter.py,sha256=CbiIjmohgtwDDTVeGzhXEGVo3XjytMdhLwU9VUkg9vo,3601
13
21
  qulab/monitor/qt_compat.py,sha256=OK71_JSO_iyXjRIKHANmaK4Lx4bILUzmXI-mwKg3QeI,788
14
22
  qulab/monitor/toolbar.py,sha256=WEag6cxAtEsOLL14XvM7pSE56EA3MO188_JuprNjdBs,7948
15
23
  qulab/scan/__init__.py,sha256=ZX4WsvqYxvJeHLgGSrtJoAnVU94gxY7EHKMxYooMERg,130
16
- qulab/scan/curd.py,sha256=thq_qfi3qng3Zx-1uhNG64IQhGCuum_LR4MOKnS8cDI,6896
17
- qulab/scan/expression.py,sha256=ngWrP1o9CuYJ1gq5YHaV7EfxKIKUX7Gz6KG80E6XThY,20070
18
- qulab/scan/models.py,sha256=5Jpo25WGMWs0GtLzYLsWO61G3-FFYx5BHhBr2b6rOTE,17681
24
+ qulab/scan/curd.py,sha256=ZcwO5SKO95OcDgpUPDHcK5FW_BrJbUzuBk71UG_pqnM,6888
25
+ qulab/scan/expression.py,sha256=Yw2_ynDpKiLeyS5m53lURFtesLkzaF9PIyAWbW-LOr0,20176
26
+ qulab/scan/models.py,sha256=JFofv-RH0gpS3--M6izXiQg7iGkS9a_em2DwhS5kTTk,17626
19
27
  qulab/scan/optimize.py,sha256=VT9TpuqDkG7FdJJkYy60r5Pfrmjvfu5i36Ru6XvIiTI,2561
20
28
  qulab/scan/query.py,sha256=-5uHMhXSyGovK1oy_uUbGVEbRFzaMBkP78ZMNfI3jD0,11809
21
29
  qulab/scan/record.py,sha256=yIHPANf6nuBXy8Igf-dMtGJ7wuFTLYlBaaAUc0AzwyU,21280
22
30
  qulab/scan/scan.py,sha256=iXvbnXLZvHa4v88MlYiZ_LYubEB7ZfbX7OiFTMhl_1o,39479
23
- qulab/scan/server.py,sha256=lb-FoGGLrTOi22f1iinimAqIJRhXJxMIJxkvdV7WiOs,19961
31
+ qulab/scan/server.py,sha256=XVwr8EC1iFhSAmecFZPWvn7N2O5MT65oZCd56GxDdIg,20035
24
32
  qulab/scan/space.py,sha256=OQLepiNNP5TNYMHXeVFT59lL5n4soPmnMoMy_o9EHt0,6696
25
33
  qulab/scan/utils.py,sha256=SzJ_c4cLZJzERZr_CJO1_4juOxjfwCpU2K1mkc1PWGM,6124
26
34
  qulab/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -46,9 +54,9 @@ qulab/sys/chat.py,sha256=DlFLP_gbVyL8fDUyad6Xu8sjQCZJauLIvF31mj5BtMs,22449
46
54
  qulab/sys/ipy_events.py,sha256=qvsqs7-CYQ-P4WtSe9UUZZ0tC88XxPMd9DM4LJALgEk,2889
47
55
  qulab/sys/progress.py,sha256=Tk1B96_1QrlI4yU0cyZlf2wVsF238DjQGqXJVG2j4t0,5350
48
56
  qulab/sys/device/__init__.py,sha256=bDqJrYQJx8YR4u_CYvJPpIBr6whP7pVnxnROTHk_yhA,149
49
- qulab/sys/device/basedevice.py,sha256=iqyX2SQLb7aRWNE5Uj2yHkFOLqVrX2I1N-xubTDB5tQ,6423
57
+ qulab/sys/device/basedevice.py,sha256=wMI93Y3Qz0oWYartWIVKoYEoOnuEaz_GRq3Ucw6brBk,6475
50
58
  qulab/sys/device/loader.py,sha256=AL0rNF4UZ-CUHoF8FNtT_ZGLwySvz7SDPsKALaJS8po,2501
51
- qulab/sys/device/utils.py,sha256=H9TvMqHZABhFdIah4uT0x9FvQ6ZPwbvw-wCnWTy4rfo,1564
59
+ qulab/sys/device/utils.py,sha256=4NG_OKMDRdVH8YKHNf6kVocevXf7bqyf1PiGctTF_oA,2314
52
60
  qulab/sys/drivers/FakeInstrument.py,sha256=kwIXN-dHmvGYBinmcCLxUQKm0ENiJpsMwgQIYNLDS2k,1867
53
61
  qulab/sys/drivers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
62
  qulab/sys/net/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -64,6 +72,7 @@ qulab/sys/rpc/client.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
64
72
  qulab/sys/rpc/exceptions.py,sha256=djK44hYVMixF2Y-lAXDGHlyyCmYGw7EJjB3I0ttejok,2567
65
73
  qulab/sys/rpc/msgpack.py,sha256=s9mbPIkQktO1FiSAuE6ymM_1w8rIERA0gMm5GQkT7z8,35327
66
74
  qulab/sys/rpc/msgpack.pyi,sha256=3dJgISPCTEBo65VaoGjcFkdLijPx6zJtKqcZiS9bcaE,1274
75
+ qulab/sys/rpc/router.py,sha256=Q10UA0cL95PcrtYU0dpPsq3hLSN9IzFaL8XCpIBSqbk,941
67
76
  qulab/sys/rpc/rpc.py,sha256=9ZwzC79FgmbHXbO1Gjg4z0wZgHLDziTwVuS2aZpLJAk,11979
68
77
  qulab/sys/rpc/serialize.py,sha256=0SR1me02mao2JwWR3rdjUIcOzh1m1JFb7fLEaw_q-pE,3355
69
78
  qulab/sys/rpc/server.py,sha256=e3R0gwOHpLEkSp7Tb43FMSDvqSG-pjrkskdISKQRseE,713
@@ -73,14 +82,16 @@ qulab/sys/rpc/worker.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
73
82
  qulab/sys/rpc/zmq_socket.py,sha256=2uc9MjCRUwWm6iPmi2VGnOYNf9yWFSUzEVYrx0jZvPU,8272
74
83
  qulab/visualization/__init__.py,sha256=26cuHt3QIJXUb3VaMxlJx3IQTOUVJFKlYBZr7WMP53M,6129
75
84
  qulab/visualization/__main__.py,sha256=9zKK3yZFy0leU40ou6BpRC1Fsetfc1gjjFzIZYIwP6Y,1639
76
- qulab/visualization/_autoplot.py,sha256=jddg40dX48Wd8G6NLFA_Kf7z1QxdrZBDS99Xx2GLMqs,14099
85
+ qulab/visualization/_autoplot.py,sha256=31B7pn1pK19abpQDGYBU9a_27cDL87LBpx9vKqlcYAo,14165
86
+ qulab/visualization/plot_circ.py,sha256=qZS3auNG4qIyUvC-ijTce17xJwGBvpRvPUqPEx64KUY,8759
77
87
  qulab/visualization/plot_layout.py,sha256=clNw9QjE_kVNpIIx2Ob4YhAz2fucPGMuzkoIrOJo_Y8,13533
78
- qulab/visualization/plot_seq.py,sha256=lphYF4VhkEdc_wWr1kFBwrx2yujkyFPFaJ3pjr61awI,2693
88
+ qulab/visualization/plot_seq.py,sha256=UWTS6p9nfX_7B8ehcYo6UnSTUCjkBsNU9jiOeW2calY,6751
79
89
  qulab/visualization/qdat.py,sha256=ZeevBYWkzbww4xZnsjHhw7wRorJCBzbG0iEu-XQB4EA,5735
90
+ qulab/visualization/rot3d.py,sha256=lMrEJlRLwYe6NMBlGkKYpp_V9CTipOAuDy6QW_cQK00,734
80
91
  qulab/visualization/widgets.py,sha256=6KkiTyQ8J-ei70LbPQZAK35wjktY47w2IveOa682ftA,3180
81
- QuLab-2.3.6.dist-info/LICENSE,sha256=PRzIKxZtpQcH7whTG6Egvzl1A0BvnSf30tmR2X2KrpA,1065
82
- QuLab-2.3.6.dist-info/METADATA,sha256=swiVtD-iwLzFtt1Aspniiz20EpamHzO5mpkz4mOq6FU,3633
83
- QuLab-2.3.6.dist-info/WHEEL,sha256=LLSyqktal3jZJhWNtlg7HaUJKl3AGKH2j3EL7wpWyD4,114
84
- QuLab-2.3.6.dist-info/entry_points.txt,sha256=ohBzutEnQimP_BZWiuXdSliu4QAYSHHcN0PZD8c7ZCY,46
85
- QuLab-2.3.6.dist-info/top_level.txt,sha256=3T886LbAsbvjonu_TDdmgxKYUn939BVTRPxPl9r4cEg,6
86
- QuLab-2.3.6.dist-info/RECORD,,
92
+ QuLab-2.4.1.dist-info/LICENSE,sha256=PRzIKxZtpQcH7whTG6Egvzl1A0BvnSf30tmR2X2KrpA,1065
93
+ QuLab-2.4.1.dist-info/METADATA,sha256=sLQ_7QutS5SAr4nKHWIk4wjJX4_fEZH9FI0gSOVfAq8,3698
94
+ QuLab-2.4.1.dist-info/WHEEL,sha256=Yd3eJSBM2hj8W-ouaiMfFUwQYAS-D6P73Ob9yN5MZd0,114
95
+ QuLab-2.4.1.dist-info/entry_points.txt,sha256=b0v1GXOwmxY-nCCsPN_rHZZvY9CtTbWqrGj8u1m8yHo,45
96
+ QuLab-2.4.1.dist-info/top_level.txt,sha256=3T886LbAsbvjonu_TDdmgxKYUn939BVTRPxPl9r4cEg,6
97
+ QuLab-2.4.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (71.0.0)
2
+ Generator: setuptools (75.8.0)
3
3
  Root-Is-Purelib: false
4
4
  Tag: cp310-cp310-macosx_10_9_universal2
5
5
 
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ qulab = qulab.__main__:cli
qulab/__main__.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import click
2
2
 
3
+ from .executor.__main__ import create, maintain, run
3
4
  from .monitor.__main__ import main as monitor
4
5
  from .scan.server import server
5
6
  from .sys.net.cli import dht
@@ -7,20 +8,23 @@ from .visualization.__main__ import plot
7
8
 
8
9
 
9
10
  @click.group()
10
- def main():
11
+ def cli():
11
12
  pass
12
13
 
13
14
 
14
- @main.command()
15
+ @cli.command()
15
16
  def hello():
16
17
  """Print hello world."""
17
18
  click.echo('hello, world')
18
19
 
19
20
 
20
- main.add_command(monitor)
21
- main.add_command(plot)
22
- main.add_command(dht)
23
- main.add_command(server)
21
+ cli.add_command(monitor)
22
+ cli.add_command(plot)
23
+ cli.add_command(dht)
24
+ cli.add_command(server)
25
+ cli.add_command(maintain)
26
+ cli.add_command(run)
27
+ cli.add_command(create)
24
28
 
25
29
  if __name__ == '__main__':
26
- main()
30
+ cli()
qulab/dicttree.py ADDED
@@ -0,0 +1,511 @@
1
+ import copy
2
+ import math
3
+ import operator
4
+ import pickle
5
+ import sys
6
+ from typing import Any, Generator
7
+
8
+
9
+ class Singleton(type):
10
+ _instances = {}
11
+
12
+ def __call__(cls, *args, **kwargs):
13
+ if cls not in cls._instances:
14
+ cls._instances[cls] = super(Singleton,
15
+ cls).__call__(*args, **kwargs)
16
+ return cls._instances[cls]
17
+
18
+
19
+ class _SELF(metaclass=Singleton):
20
+ __slots__ = ()
21
+
22
+ def __repr__(self):
23
+ return "self"
24
+
25
+
26
+ class _NOTSET(metaclass=Singleton):
27
+ __slots__ = ()
28
+
29
+ def __repr__(self):
30
+ return 'N/A'
31
+
32
+
33
+ class _UNKNOW(metaclass=Singleton):
34
+ __slots__ = ()
35
+
36
+ def __repr__(self) -> str:
37
+ return "Unknow"
38
+
39
+
40
+ class _DELETE(metaclass=Singleton):
41
+ __slots__ = ()
42
+
43
+ def __repr__(self):
44
+ return 'Delete'
45
+
46
+
47
+ SELF = _SELF()
48
+ NOTSET = _NOTSET()
49
+ UNKNOW = _UNKNOW()
50
+ DELETE = _DELETE()
51
+
52
+
53
+ def flattenDictIter(d: dict,
54
+ prefix: list = []
55
+ ) -> Generator[tuple[str, Any], None, None]:
56
+ for k in d:
57
+ if isinstance(d[k], dict):
58
+ yield from flattenDictIter(d[k], prefix=[*prefix, k])
59
+ else:
60
+ yield '.'.join(prefix + [k]), d[k]
61
+
62
+
63
+ def flattenDict(d: dict[str, Any]) -> dict[str, Any]:
64
+ return {k: v for k, v in flattenDictIter(d)}
65
+
66
+
67
+ def foldDict(d: dict[str, Any]) -> dict[str, Any]:
68
+ ret = {}
69
+
70
+ for k, v in d.items():
71
+ keys = k.split('.')
72
+ d = ret
73
+ parent = None
74
+
75
+ for key in keys[:-1]:
76
+ if not isinstance(d, dict):
77
+ parent[0][parent[1]] = {SELF: d}
78
+ d = parent[0][parent[1]]
79
+ if key not in d:
80
+ d[key] = dict()
81
+ parent = d, key
82
+ d = d[key]
83
+ if not isinstance(d, dict):
84
+ parent[0][parent[1]] = {SELF: d}
85
+ d = parent[0][parent[1]]
86
+ if keys[-1] in d and isinstance(d[keys[-1]], dict):
87
+ d[keys[-1]][SELF] = v
88
+ else:
89
+ d[keys[-1]] = v
90
+ return ret
91
+
92
+
93
+ class Update():
94
+ __slots__ = ('o', 'n')
95
+
96
+ def __init__(self, o, n):
97
+ self.o = o
98
+ self.n = n
99
+
100
+ def __repr__(self):
101
+ return f"Update: {self.o!r} ==> {self.n!r}"
102
+
103
+ def __eq__(self, o: object) -> bool:
104
+ return isinstance(o, Update) and self.n == o.n
105
+
106
+
107
+ class Create():
108
+ __slots__ = ('n', 'replace')
109
+
110
+ def __init__(self, n, replace=False):
111
+ """
112
+ Create a new node
113
+
114
+ :param n: the new node
115
+ :param replace: if True, replace the node if it already exists
116
+ """
117
+ self.n = n
118
+ self.replace = replace
119
+
120
+ def __repr__(self):
121
+ if self.replace:
122
+ return f"Replace: {self.n!r}"
123
+ else:
124
+ return f"Create: {self.n!r}"
125
+
126
+ def __eq__(self, o: object) -> bool:
127
+ return isinstance(
128
+ o, Create) and self.n == o.n and self.replace == o.replace
129
+
130
+
131
+ def _eq(a, b):
132
+ import numpy as np
133
+
134
+ if isinstance(a, np.ndarray):
135
+ return np.array_equal(a, np.asarray(b))
136
+
137
+ try:
138
+ return a == b
139
+ except:
140
+ pass
141
+ if isinstance(a, (list, tuple)):
142
+ return len(a) == len(b) and all(_eq(a[i], b[i]) for i in range(len(a)))
143
+ if isinstance(a, dict):
144
+ return set(a.keys()) == set(b.keys()) and all(
145
+ _eq(a[k], b[k]) for k in a)
146
+
147
+ try:
148
+ return pickle.dumps(a) == pickle.dumps(b)
149
+ except:
150
+ return False
151
+
152
+
153
+ def diff(d1: dict, d2: dict) -> dict:
154
+ """
155
+ Compute the difference between two dictionaries
156
+
157
+ Args:
158
+ d1: the original dictionary
159
+ d2: the new dictionary
160
+
161
+ Returns:
162
+ a dictionary containing the difference between d1 and d2
163
+ """
164
+ ret = {}
165
+ for k in d2:
166
+ if k in d1:
167
+ if isinstance(d2[k], type(d1[k])) and _eq(d1[k], d2[k]):
168
+ pass
169
+ elif isinstance(d1[k], dict) and isinstance(d2[k], dict):
170
+ ret[k] = diff(d1[k], d2[k])
171
+ else:
172
+ ret[k] = Update(d1[k], d2[k])
173
+ else:
174
+ ret[k] = Create(d2[k])
175
+ for k in d1:
176
+ if k not in d2:
177
+ ret[k] = DELETE
178
+ return ret
179
+
180
+
181
+ def patch(source, diff, in_place=False):
182
+ """
183
+ Patch a dictionary with a diff
184
+
185
+ Args:
186
+ source: the original dictionary
187
+ diff: the diff
188
+ in_place: if True, patch the source dictionary in place
189
+
190
+ Returns:
191
+ the patched dictionary
192
+ """
193
+ if in_place:
194
+ ret = source
195
+ else:
196
+ ret = copy.copy(source)
197
+ for k, v in diff.items():
198
+ if isinstance(v, dict):
199
+ ret[k] = patch(source[k], v, in_place=in_place)
200
+ else:
201
+ if isinstance(v, Update):
202
+ ret[k] = v.n
203
+ elif isinstance(v, Create):
204
+ if v.replace or k not in ret:
205
+ ret[k] = v.n
206
+ else:
207
+ update_tree(ret[k], v.n)
208
+ elif v is DELETE:
209
+ del ret[k]
210
+ else:
211
+ raise ValueError(f"Unsupported patch: {v!r}")
212
+ return ret
213
+
214
+
215
+ def merge(diff1, diff2, origin=None):
216
+ """
217
+ Merge two diffs
218
+
219
+ Args:
220
+ diff1: the first diff
221
+ diff2: the second diff
222
+ origin: the original dictionary
223
+
224
+ Returns:
225
+ the merged diff
226
+ """
227
+ if origin is not None:
228
+ updated = patch(patch(origin, diff1), diff2)
229
+ return diff(origin, updated)
230
+
231
+ ret = {}
232
+ for k, v in diff1.items():
233
+ if k in diff2:
234
+ v2 = diff2[k]
235
+ if isinstance(v, dict) and isinstance(v2, dict):
236
+ d = merge(v, v2)
237
+ if d:
238
+ ret[k] = d
239
+ else:
240
+ if isinstance(v, Update) and isinstance(v2, Update):
241
+ ret[k] = Update(v.o, v2.n)
242
+ elif isinstance(v, Create) and isinstance(v2, dict):
243
+ ret[k] = Create(patch(copy.copy(v.n), v2, True))
244
+ elif isinstance(v, Create) and isinstance(v2, Update):
245
+ ret[k] = Create(v2.n)
246
+ elif isinstance(v, Create) and v2 is DELETE:
247
+ pass
248
+ elif v2 is DELETE:
249
+ ret[k] = DELETE
250
+ elif v is DELETE and isinstance(v2, Create):
251
+ if isinstance(v2.n, dict):
252
+ ret[k] = Create(v2.n, replace=True)
253
+ else:
254
+ ret[k] = Update(UNKNOW, v2.n)
255
+ elif isinstance(v2, Create) and v2.replace:
256
+ ret[k] = v2
257
+ else:
258
+ raise ValueError(f"Unsupported merge: {v!r} {v2!r}")
259
+ else:
260
+ ret[k] = v
261
+ for k, v in diff2.items():
262
+ if k not in diff1:
263
+ ret[k] = v
264
+ return ret
265
+
266
+
267
+ def print_diff(d, limit=None, offset=0, file=sys.stdout, ignores=None):
268
+ """
269
+ Print a diff
270
+
271
+ Args:
272
+ d: the diff
273
+ limit: the maximum number of lines to print
274
+ offset: the offset of the first line
275
+ file: the file to print to
276
+ ignores: ignore keys starting with this prefix
277
+ """
278
+ count = 0
279
+ for i, (k, v) in enumerate(flattenDictIter(d)):
280
+ if count >= offset:
281
+ if ignores is not None and k.startswith(ignores):
282
+ continue
283
+ print(f"{k:40}", v, file=file)
284
+ count += 1
285
+ if limit is not None and count >= limit:
286
+ break
287
+
288
+
289
+ def update_tree(result, updates):
290
+ for k, v in updates.items():
291
+ if isinstance(v, dict):
292
+ if k not in result or not isinstance(result[k], dict):
293
+ result[k] = {}
294
+ update_tree(result[k], v)
295
+ else:
296
+ result[k] = v
297
+ return result
298
+
299
+
300
+ def queryref_tree(q, keys, dct, prefix=[], chain=None):
301
+ q = q[1:]
302
+ if q.startswith('.'):
303
+ while q.startswith('.'):
304
+ keys.pop()
305
+ q = q[1:]
306
+ q = '.'.join(keys + [q])
307
+
308
+ if chain is None:
309
+ chain = [q]
310
+ elif q in chain:
311
+ raise KeyError(f'Circular reference: {chain+[q]}')
312
+
313
+ return query_tree(q, dct, prefix=prefix, eval=True, chain=chain + [q])
314
+
315
+
316
+ def query_tree(q, dct, prefix=[], eval=True, chain=None):
317
+ ret = dct
318
+ keys = q.split('.')
319
+ for i, key in enumerate(keys):
320
+ if key not in ret:
321
+ return (NOTSET, '.'.join(prefix + keys[:i + 1]))
322
+ ret = ret[key]
323
+ if isinstance(ret, str) and eval:
324
+ if ret.startswith('$'):
325
+ return queryref_tree(ret, keys, dct, prefix=prefix, chain=chain)
326
+ elif ret.startswith('&'):
327
+ return eval_expr(ret[1:], Env(dct,
328
+ keys,
329
+ prefix=prefix,
330
+ chain=chain))
331
+ return ret
332
+
333
+
334
+ class Env():
335
+
336
+ def __init__(self, dct=None, keys=None, prefix=[], chain=None):
337
+ self.dct = dct if dct is not None else {}
338
+ self.keys = keys if keys is not None else []
339
+ self.chain = chain
340
+ self.prefix = prefix
341
+
342
+ def get(self, name):
343
+ return queryref_tree(name[1:],
344
+ self.keys,
345
+ self.dct,
346
+ prefix=self.prefix,
347
+ chain=self.chain)
348
+
349
+
350
+ internal_functions = {
351
+ '**': operator.pow,
352
+ '+': operator.add,
353
+ '-': operator.sub,
354
+ '*': operator.mul,
355
+ '/': operator.truediv,
356
+ '//': operator.floordiv,
357
+ '%': operator.mod,
358
+ '&': operator.and_,
359
+ '|': operator.or_,
360
+ '^': operator.xor,
361
+ '~': operator.invert,
362
+ '<<': operator.lshift,
363
+ '>>': operator.rshift,
364
+ 'getitem': operator.getitem,
365
+ 'contains': operator.contains,
366
+ 'not': operator.not_,
367
+ 'getattr': getattr,
368
+ 'abs': abs,
369
+ 'min': min,
370
+ 'max': max,
371
+ 'sum': sum,
372
+ 'len': len,
373
+ 'any': any,
374
+ 'all': all,
375
+ 'ord': ord,
376
+ 'chr': chr,
377
+ 'bin': bin,
378
+ 'oct': oct,
379
+ 'hex': hex,
380
+ 'int': int,
381
+ 'round': round,
382
+ 'real': lambda x: x.real,
383
+ 'imag': lambda x: x.imag,
384
+ 'sqrt': math.sqrt,
385
+ 'sin': math.sin,
386
+ 'cos': math.cos,
387
+ 'tan': math.tan,
388
+ 'asin': math.asin,
389
+ 'acos': math.acos,
390
+ 'atan': math.atan,
391
+ 'atan2': math.atan2,
392
+ 'sinh': math.sinh,
393
+ 'cosh': math.cosh,
394
+ 'tanh': math.tanh,
395
+ 'asinh': math.asinh,
396
+ 'acosh': math.acosh,
397
+ 'atanh': math.atanh,
398
+ 'ceil': math.ceil,
399
+ 'floor': math.floor,
400
+ 'trunc': math.trunc,
401
+ 'log10': math.log10,
402
+ 'log': math.log,
403
+ 'exp': math.exp,
404
+ }
405
+
406
+
407
+ def eval_expr(expression, env=None, functions=None):
408
+ from pyparsing import (Forward, Literal, ParseResults, Suppress, Word,
409
+ alphanums, delimitedList, infixNotation, oneOf,
410
+ opAssoc, pyparsing_common)
411
+ if functions is None:
412
+ functions = internal_functions
413
+
414
+ def lookup(name):
415
+ return env.get(name)
416
+
417
+ def apply(fun, *args):
418
+ return functions[fun](*args)
419
+
420
+ def eval_expr(t):
421
+ if not isinstance(t, ParseResults):
422
+ return t
423
+ if t[0] == '-' and len(t) == 2:
424
+ return -t[1]
425
+ return apply(t[1], t[0], t[2])
426
+
427
+ # Define pyparsing grammar
428
+ expr = Forward()
429
+
430
+ complex_ = (pyparsing_common.number +
431
+ 'j').setParseAction(lambda s, l, t: complex(t[0]))
432
+ number = pyparsing_common.number | complex_
433
+
434
+ const = oneOf('pi e').setParseAction(lambda s, l, t: {
435
+ 'pi': math.pi,
436
+ 'e': math.e
437
+ }.get(t[0]))
438
+
439
+ variable = Word("$", alphanums +
440
+ "._").setParseAction(lambda s, l, t: lookup(t[0]))
441
+
442
+ bracket = Suppress('(') + expr + Suppress(')')
443
+ bracket.setParseAction(lambda s, l, t: t[0])
444
+
445
+ operand = const | variable | number | bracket
446
+
447
+ func = pyparsing_common.identifier
448
+ func_call = func + Suppress("(") + delimitedList(expr) + Suppress(")")
449
+ func_call.setParseAction(lambda s, l, t: apply(t[0], *t[1:]))
450
+
451
+ term = operand | func_call
452
+
453
+ expr << infixNotation(term, [
454
+ (Literal('**'), 2, opAssoc.RIGHT),
455
+ (Literal('~'), 1, opAssoc.RIGHT),
456
+ (Literal('-'), 1, opAssoc.RIGHT),
457
+ (oneOf('* / // %'), 2, opAssoc.LEFT),
458
+ (oneOf('+ -'), 2, opAssoc.LEFT),
459
+ (oneOf('>> <<'), 2, opAssoc.LEFT),
460
+ (Literal('&'), 2, opAssoc.LEFT),
461
+ (Literal('^'), 2, opAssoc.LEFT),
462
+ (Literal('|'), 2, opAssoc.LEFT),
463
+ ])
464
+
465
+ expr.setParseAction(lambda s, l, t: eval_expr(t[0]))
466
+
467
+ parsed = expr.parseString(expression, parseAll=True)
468
+ return parsed[0]
469
+
470
+
471
+ def sorted_tree(dct, *, keys=None):
472
+ if keys is None or callable(keys):
473
+ key = keys
474
+ elif isinstance(keys, list):
475
+ key = keys[0]
476
+ if len(keys) > 1:
477
+ keys = keys[1:]
478
+ else:
479
+ keys = None
480
+ elif (isinstance(keys, tuple) and len(keys) == 2
481
+ and isinstance(keys[1], dict)):
482
+ key = keys[0]
483
+ keys = keys[1]
484
+ else:
485
+ raise Exception(f"Unsupported keys: {keys!r}")
486
+
487
+ if isinstance(dct, dict):
488
+ if isinstance(keys, dict):
489
+ default = keys.get('default', None)
490
+ return {
491
+ k: sorted_tree(dct[k], keys=keys.get(k, default))
492
+ for k in sorted(dct.keys(), key=key)
493
+ }
494
+ else:
495
+ return {
496
+ k: sorted_tree(dct[k], keys=keys)
497
+ for k in sorted(dct.keys(), key=key)
498
+ }
499
+ elif isinstance(dct, set):
500
+ if isinstance(keys, dict):
501
+ default = keys.get('default', None)
502
+ return set([
503
+ sorted_tree(v, keys=keys.get(v, default))
504
+ for v in sorted(list(dct), key=key)
505
+ ])
506
+ else:
507
+ return set([
508
+ sorted_tree(v, keys=keys) for v in sorted(list(dct), key=key)
509
+ ])
510
+ else:
511
+ return dct