QuLab 2.3.2__cp311-cp311-macosx_10_9_universal2.whl → 2.3.4__cp311-cp311-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
1
  Metadata-Version: 2.1
2
2
  Name: QuLab
3
- Version: 2.3.2
3
+ Version: 2.3.4
4
4
  Summary: contral instruments and manage data
5
5
  Author-email: feihoo87 <feihoo87@gmail.com>
6
6
  Maintainer-email: feihoo87 <feihoo87@gmail.com>
@@ -38,6 +38,9 @@ Requires-Dist: ply >=3.11
38
38
  Requires-Dist: pyzmq >=25.1.0
39
39
  Requires-Dist: scipy >=1.0.0
40
40
  Requires-Dist: watchdog >=4.0.0
41
+ Provides-Extra: full
42
+ Requires-Dist: SQLAlchemy >=2.0.19 ; extra == 'full'
43
+ Requires-Dist: uvloop >=0.19.0 ; extra == 'full'
41
44
 
42
45
  # QuLab
43
46
  [![View build status](https://travis-ci.org/feihoo87/QuLab.svg?branch=master)](https://travis-ci.org/feihoo87/QuLab)
@@ -1,7 +1,7 @@
1
1
  qulab/__init__.py,sha256=P-Mx2p4TVmL91SoxoeXcj8Qm0x4xUf5Q_FLk0Yc_gIQ,138
2
2
  qulab/__main__.py,sha256=ZC1NKaoxKyy60DaCfB8vYnB1z3RXQ2j8E1sRZ4A8sXE,428
3
- qulab/fun.cpython-311-darwin.so,sha256=aFDKwsfj4iIn_qPA_HgPOb23YQ_AwnE5G_fPkQEbQIs,159616
4
- qulab/version.py,sha256=wmsQzkE8l2rmrt5-AjUYaTVQ_X-dbo-FDvqay4JDxVk,21
3
+ qulab/fun.cpython-311-darwin.so,sha256=VAuzZloDPT2N99qUrQIFhgFcr9_xk3BSQ_7PXUyDVJ4,159616
4
+ qulab/version.py,sha256=MtmOJMh3yHY9aTdRsZDgbpPfbzm9xdycdi9jjMKzYjM,21
5
5
  qulab/monitor/__init__.py,sha256=nTHelnDpxRS_fl_B38TsN0njgq8eVTEz9IAnN3NbDlM,42
6
6
  qulab/monitor/__main__.py,sha256=w3yUcqq195LzSnXTkQcuC1RSFRhy4oQ_PEBmucXguME,97
7
7
  qulab/monitor/config.py,sha256=fQ5JcsMApKc1UwANEnIvbDQZl8uYW0tle92SaYtX9lI,744
@@ -14,15 +14,15 @@ qulab/monitor/qt_compat.py,sha256=OK71_JSO_iyXjRIKHANmaK4Lx4bILUzmXI-mwKg3QeI,78
14
14
  qulab/monitor/toolbar.py,sha256=WEag6cxAtEsOLL14XvM7pSE56EA3MO188_JuprNjdBs,7948
15
15
  qulab/scan/__init__.py,sha256=ZX4WsvqYxvJeHLgGSrtJoAnVU94gxY7EHKMxYooMERg,130
16
16
  qulab/scan/curd.py,sha256=thq_qfi3qng3Zx-1uhNG64IQhGCuum_LR4MOKnS8cDI,6896
17
- qulab/scan/expression.py,sha256=s2_sWFm4gKgJ7pGLmuugCU8XifAm9Mc3AT3bdoyFUtM,19459
17
+ qulab/scan/expression.py,sha256=ngWrP1o9CuYJ1gq5YHaV7EfxKIKUX7Gz6KG80E6XThY,20070
18
18
  qulab/scan/models.py,sha256=5Jpo25WGMWs0GtLzYLsWO61G3-FFYx5BHhBr2b6rOTE,17681
19
19
  qulab/scan/optimize.py,sha256=jYceGazEabuwYqmZE8P6Nnq_KsLyUP0p-p88VbbJLhM,2559
20
20
  qulab/scan/query.py,sha256=-5uHMhXSyGovK1oy_uUbGVEbRFzaMBkP78ZMNfI3jD0,11809
21
- qulab/scan/record.py,sha256=sICMLAWf3IgvJ3JlS0cKQA4mSpLZins7K5qFoAnCSYI,21194
22
- qulab/scan/scan.py,sha256=KjQyq42xFi9wCjFTu26cXzV5_iii2X0dceqzL1s5w3Q,37834
23
- qulab/scan/server.py,sha256=ZT-J447xuLSpmbTNm5dNIz8_CXKOow66_bjkFNGh_vI,14274
21
+ qulab/scan/record.py,sha256=yIHPANf6nuBXy8Igf-dMtGJ7wuFTLYlBaaAUc0AzwyU,21280
22
+ qulab/scan/scan.py,sha256=hrCnoIROMPoYXEZnU9GTZUR-aX9qP58d40wlN3DTQCE,40602
23
+ qulab/scan/server.py,sha256=_4YrOJcnqDTnxryZXE8lEKGzuT4LS-8q9bN6jrTEqUQ,19950
24
24
  qulab/scan/space.py,sha256=X5anTvw7X0XGN83y0o0C-zhvh6bDEU-kshGgo004pNE,6076
25
- qulab/scan/utils.py,sha256=Pg_tCf3SUKTiPSBqb6Enkgx4bAyQJAkDGe9uYys1xVU,3613
25
+ qulab/scan/utils.py,sha256=zCV_Td8hBgb16Hv3XKo64KYDDpvBw2rQ83fxq1hvjG8,4586
26
26
  qulab/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
27
  qulab/storage/__main__.py,sha256=3emxxRry8BB0m8hUZvJ_oBqkPy7ksV7flHB_KEDXZuI,1692
28
28
  qulab/storage/base_dataset.py,sha256=4aKhqBNdZfdlm_z1Qy5dv0HrvgpvMdy8pbyfua8uE-4,11865
@@ -78,9 +78,9 @@ qulab/visualization/plot_layout.py,sha256=clNw9QjE_kVNpIIx2Ob4YhAz2fucPGMuzkoIrO
78
78
  qulab/visualization/plot_seq.py,sha256=lphYF4VhkEdc_wWr1kFBwrx2yujkyFPFaJ3pjr61awI,2693
79
79
  qulab/visualization/qdat.py,sha256=ZeevBYWkzbww4xZnsjHhw7wRorJCBzbG0iEu-XQB4EA,5735
80
80
  qulab/visualization/widgets.py,sha256=6KkiTyQ8J-ei70LbPQZAK35wjktY47w2IveOa682ftA,3180
81
- QuLab-2.3.2.dist-info/LICENSE,sha256=PRzIKxZtpQcH7whTG6Egvzl1A0BvnSf30tmR2X2KrpA,1065
82
- QuLab-2.3.2.dist-info/METADATA,sha256=mLdXTnca-RyYTycpdpxKFyrFe9UPysvw5sBDoGhqcAg,3510
83
- QuLab-2.3.2.dist-info/WHEEL,sha256=UDBB_KFYXAT_a6Q3uGzMOBYEG2sfuzNdKs9Nu_rq9v4,114
84
- QuLab-2.3.2.dist-info/entry_points.txt,sha256=ohBzutEnQimP_BZWiuXdSliu4QAYSHHcN0PZD8c7ZCY,46
85
- QuLab-2.3.2.dist-info/top_level.txt,sha256=3T886LbAsbvjonu_TDdmgxKYUn939BVTRPxPl9r4cEg,6
86
- QuLab-2.3.2.dist-info/RECORD,,
81
+ QuLab-2.3.4.dist-info/LICENSE,sha256=PRzIKxZtpQcH7whTG6Egvzl1A0BvnSf30tmR2X2KrpA,1065
82
+ QuLab-2.3.4.dist-info/METADATA,sha256=Noz4IplxqE9TBtcN2Qr38B1J0vtpEkCWsjAjvRMGF_I,3633
83
+ QuLab-2.3.4.dist-info/WHEEL,sha256=UDBB_KFYXAT_a6Q3uGzMOBYEG2sfuzNdKs9Nu_rq9v4,114
84
+ QuLab-2.3.4.dist-info/entry_points.txt,sha256=ohBzutEnQimP_BZWiuXdSliu4QAYSHHcN0PZD8c7ZCY,46
85
+ QuLab-2.3.4.dist-info/top_level.txt,sha256=3T886LbAsbvjonu_TDdmgxKYUn939BVTRPxPl9r4cEg,6
86
+ QuLab-2.3.4.dist-info/RECORD,,
Binary file
qulab/scan/expression.py CHANGED
@@ -419,7 +419,7 @@ class Expression():
419
419
  def is_const(self, env) -> bool:
420
420
  return False
421
421
 
422
- def value(self, env):
422
+ def value(self, env=_default_env):
423
423
  if self.changed(env):
424
424
  self.cache = self.eval(env)
425
425
  return self.cache
@@ -612,3 +612,31 @@ class Symbol(Expression):
612
612
 
613
613
  def __repr__(self) -> str:
614
614
  return self.name
615
+
616
+
617
+ sin = Symbol('sin')
618
+ cos = Symbol('cos')
619
+ tan = Symbol('tan')
620
+ pi = Symbol('pi')
621
+ e = Symbol('e')
622
+ log = Symbol('log')
623
+ log2 = Symbol('log2')
624
+ log10 = Symbol('log10')
625
+ exp = Symbol('exp')
626
+ sqrt = Symbol('sqrt')
627
+ abs = Symbol('abs')
628
+ sinh = Symbol('sinh')
629
+ cosh = Symbol('cosh')
630
+ tanh = Symbol('tanh')
631
+ arcsin = Symbol('arcsin')
632
+ arccos = Symbol('arccos')
633
+ arctan = Symbol('arctan')
634
+ arctan2 = Symbol('arctan2')
635
+ arcsinh = Symbol('arcsinh')
636
+ arccosh = Symbol('arccosh')
637
+ arctanh = Symbol('arctanh')
638
+ sinc = Symbol('sinc')
639
+ sign = Symbol('sign')
640
+ heaviside = Symbol('heaviside')
641
+ erf = Symbol('erf')
642
+ erfc = Symbol('erfc')
qulab/scan/record.py CHANGED
@@ -474,7 +474,9 @@ class Record():
474
474
  'method': 'record_keys',
475
475
  'record_id': self.id
476
476
  })
477
- return socket.recv_pyobj()
477
+ keys = socket.recv_pyobj()
478
+ self._items = {key: None for key in keys}
479
+ return keys
478
480
  else:
479
481
  return list(self._items.keys())
480
482
 
qulab/scan/scan.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import contextlib
2
3
  import copy
3
4
  import inspect
4
5
  import itertools
@@ -25,7 +26,7 @@ from .optimize import NgOptimizer
25
26
  from .record import Record
26
27
  from .server import default_record_port
27
28
  from .space import Optimizer, OptimizeSpace, Space
28
- from .utils import async_zip, call_function, dump_globals
29
+ from .utils import async_zip, call_function, dump_dict, dump_globals
29
30
 
30
31
  try:
31
32
  from tqdm.notebook import tqdm
@@ -263,6 +264,7 @@ class Scan():
263
264
  self._current_level = 0
264
265
  self._variables = {}
265
266
  self._main_task = None
267
+ self._background_tasks = ()
266
268
  self._sock = None
267
269
  self._sem = asyncio.Semaphore(max_promise + 1)
268
270
  self._bar: dict[int, tqdm] = {}
@@ -286,6 +288,7 @@ class Scan():
286
288
  del state['record']
287
289
  del state['_sock']
288
290
  del state['_main_task']
291
+ del state['_background_tasks']
289
292
  del state['_bar']
290
293
  del state['_msg_queue']
291
294
  del state['_prm_queue']
@@ -298,6 +301,7 @@ class Scan():
298
301
  self.record = None
299
302
  self._sock = None
300
303
  self._main_task = None
304
+ self._background_tasks = ()
301
305
  self._bar = {}
302
306
  self._prm_queue = asyncio.Queue()
303
307
  self._msg_queue = asyncio.Queue(self._max_message)
@@ -331,6 +335,9 @@ class Scan():
331
335
  await _unpack(key, variables)
332
336
  elif inspect.isawaitable(value) and not self.hiden(key):
333
337
  variables[key] = await value
338
+
339
+ if self.record is None:
340
+ self.record = await self.create_record()
334
341
  if self._sock is not None:
335
342
  await self._sock.send_pyobj({
336
343
  'task': self.id,
@@ -374,20 +381,37 @@ class Scan():
374
381
  return True
375
382
 
376
383
  async def create_record(self):
377
- if self._sock is not None:
378
- await self._sock.send_pyobj({
379
- 'task':
380
- self.id,
381
- 'method':
382
- 'record_create',
383
- 'description':
384
- dill.dumps(self.description)
385
- })
384
+ if self._sock is None:
385
+ return Record(None, self.description['database'], self.description)
386
+
387
+ if self.config:
388
+ self.description['config'] = await create_config(
389
+ self.config, self.description['database'], self._sock)
390
+ if current_notebook() is None:
391
+ await create_notebook('untitle', self.description['database'],
392
+ self._sock)
393
+ cell_id = await save_input_cells(current_notebook(),
394
+ self.description['entry']['scripts'],
395
+ self.description['database'],
396
+ self._sock)
397
+ self.description['entry']['scripts'] = cell_id
398
+
399
+ await self._sock.send_pyobj({
400
+ 'task':
401
+ self.id,
402
+ 'method':
403
+ 'record_create',
404
+ 'description':
405
+ dump_dict(self.description,
406
+ keys=[
407
+ 'intrinsic_loops', 'app', 'tags', 'loops',
408
+ 'independent_variables', 'axis', 'config', 'entry'
409
+ ])
410
+ })
386
411
 
387
- record_id = await self._sock.recv_pyobj()
388
- return Record(record_id, self.description['database'],
389
- self.description)
390
- return Record(None, self.description['database'], self.description)
412
+ record_id = await self._sock.recv_pyobj()
413
+ return Record(record_id, self.description['database'],
414
+ self.description)
391
415
 
392
416
  def get(self, name: str):
393
417
  if name in self.description['consts']:
@@ -481,7 +505,7 @@ class Scan():
481
505
  elif isinstance(space, OptimizeSpace):
482
506
  space.name = name
483
507
  space.optimizer.dimensions[name] = space.space
484
- if space.suggestion:
508
+ if space.suggestion is not None:
485
509
  space.optimizer.suggestion[name] = space.suggestion
486
510
  self._add_search_space(name, space.optimizer.level, space)
487
511
  self.add_depends(space.optimizer.name, [name])
@@ -582,8 +606,34 @@ class Scan():
582
606
  await task
583
607
  self._msg_queue.task_done()
584
608
 
609
+ @contextlib.asynccontextmanager
610
+ async def _send_msg_and_update_bar(self):
611
+ send_msg_task = asyncio.create_task(self._send_msg())
612
+ update_progress_task = asyncio.create_task(self._update_progress())
613
+ try:
614
+ yield (send_msg_task, update_progress_task)
615
+ finally:
616
+ update_progress_task.cancel()
617
+ send_msg_task.cancel()
618
+ while True:
619
+ try:
620
+ task = self._prm_queue.get_nowait()
621
+ except:
622
+ break
623
+ try:
624
+ task.cancel()
625
+ except:
626
+ pass
627
+
628
+ async def _check_background_tasks(self):
629
+ for task in self._background_tasks:
630
+ if task.done():
631
+ await task
632
+
585
633
  async def run(self):
586
634
  assymbly(self.description)
635
+ self._background_tasks = ()
636
+
587
637
  if isinstance(
588
638
  self.description['database'],
589
639
  str) and self.description['database'].startswith("tcp://"):
@@ -591,27 +641,17 @@ class Scan():
591
641
  connect=self.description['database'],
592
642
  socket=self._sock) as socket:
593
643
  self._sock = socket
594
- if self.config:
595
- self.description['config'] = await create_config(
596
- self.config, self.description['database'], self._sock)
597
- if current_notebook() is None:
598
- await create_notebook('untitle',
599
- self.description['database'],
600
- self._sock)
601
- cell_id = await save_input_cells(
602
- current_notebook(), self.description['entry']['scripts'],
603
- self.description['database'], self._sock)
604
- self.description['entry']['scripts'] = cell_id
605
- await self._run()
644
+ async with self._send_msg_and_update_bar() as background_tasks:
645
+ self._background_tasks = background_tasks
646
+ await self._run()
606
647
  else:
607
648
  if self.config:
608
649
  self.description['config'] = copy.deepcopy(self.config)
609
- await self._run()
650
+ async with self._send_msg_and_update_bar() as background_tasks:
651
+ self._background_tasks = background_tasks
652
+ await self._run()
610
653
 
611
654
  async def _run(self):
612
- send_msg_task = asyncio.create_task(self._send_msg())
613
- update_progress_task = asyncio.create_task(self._update_progress())
614
-
615
655
  self._variables = {'self': self, 'config': self.config}
616
656
 
617
657
  consts = {}
@@ -633,12 +673,11 @@ class Scan():
633
673
  self.description['functions'], self.variables)
634
674
  await update_variables(self.variables, updates,
635
675
  self.description['setters'])
636
-
637
- self.record = await self.create_record()
676
+ await self._check_background_tasks()
638
677
  await self.work()
639
678
  for level, bar in self._bar.items():
640
679
  bar.close()
641
-
680
+ await self._check_background_tasks()
642
681
  if self._single_step:
643
682
  self.variables.update(await call_many_functions(
644
683
  self.description['order'].get(-1, []),
@@ -646,11 +685,9 @@ class Scan():
646
685
 
647
686
  await self.emit(0, 0, 0, self.variables)
648
687
  await self.emit(-1, 0, 0, {})
649
-
688
+ await self._check_background_tasks()
650
689
  await self._prm_queue.join()
651
- update_progress_task.cancel()
652
690
  await self._msg_queue.join()
653
- send_msg_task.cancel()
654
691
  return self.variables
655
692
 
656
693
  async def done(self):
@@ -712,6 +749,7 @@ class Scan():
712
749
  | {'config': self._synchronize_config},
713
750
  self.description['optimizers'], self.description['setters'],
714
751
  self.description['getters']):
752
+ await self._check_background_tasks()
715
753
  self._current_level += 1
716
754
  if await self._filter(variables, self.current_level - 1):
717
755
  yield variables
@@ -723,11 +761,13 @@ class Scan():
723
761
  self._current_level -= 1
724
762
  self._prm_queue.put_nowait(
725
763
  self._update_progress_bar(self.current_level, 1))
764
+ await self._check_background_tasks()
726
765
  if self.current_level == 0:
727
766
  await self.emit(self.current_level - 1, 0, 0, {})
728
767
  for name, value in self.variables.items():
729
768
  if inspect.isawaitable(value):
730
769
  self.variables[name] = await value
770
+ await self._check_background_tasks()
731
771
  await self._prm_queue.join()
732
772
 
733
773
  async def work(self, **kwds):
@@ -776,10 +816,10 @@ class Scan():
776
816
  buf = dill.dumps((awaitable, args, kwds))
777
817
  task = asyncio.get_running_loop().run_in_executor(
778
818
  self._executors, _run_function_in_process, buf)
779
- self._prm_queue.put_nowait(task)
780
- return Promise(task)
781
819
  except:
782
820
  return awaitable(*args, **kwds)
821
+ self._prm_queue.put_nowait(task)
822
+ return Promise(task)
783
823
  else:
784
824
  return awaitable
785
825
 
@@ -788,7 +828,7 @@ class Scan():
788
828
  return await awaitable
789
829
 
790
830
 
791
- def assymbly(description):
831
+ def _get_environment(description):
792
832
  import __main__
793
833
  from IPython import get_ipython
794
834
 
@@ -818,6 +858,10 @@ def assymbly(description):
818
858
 
819
859
  description['entry']['env'] = {k: v for k, v in os.environ.items()}
820
860
 
861
+ return description
862
+
863
+
864
+ def _mapping_levels(description):
821
865
  mapping = {
822
866
  label: level
823
867
  for level, label in enumerate(
@@ -850,8 +894,23 @@ def assymbly(description):
850
894
  len(space))
851
895
  except:
852
896
  pass
897
+ return levels
898
+
899
+
900
+ def _get_independent_variables(description):
901
+ independent_variables = set(description['intrinsic_loops'].keys())
902
+ for level, loops in description['loops'].items():
903
+ for name, iterable in loops:
904
+ if isinstance(iterable, (np.ndarray, list, tuple, range, Space)):
905
+ independent_variables.add(name)
906
+ return independent_variables
907
+
853
908
 
909
+ def _build_dependents(description, levels, independent_variables):
854
910
  dependents = copy.deepcopy(description['dependents'])
911
+ all_nodes = set(description['dependents'].keys())
912
+ for key, deps in dependents.items():
913
+ all_nodes.update(deps)
855
914
 
856
915
  for level in levels:
857
916
  range_list = description['loops'].get(level, [])
@@ -864,6 +923,13 @@ def assymbly(description):
864
923
  dependents[name] = set()
865
924
  dependents[name].add(f'#__loop_{level}')
866
925
 
926
+ after_yield = set()
927
+ for key in all_nodes:
928
+ if key not in independent_variables and key not in description[
929
+ 'consts']:
930
+ if key not in dependents:
931
+ after_yield.add(key)
932
+
867
933
  def _get_all_depends(key, graph):
868
934
  ret = set()
869
935
  if key not in graph:
@@ -877,7 +943,13 @@ def assymbly(description):
877
943
  full_depends = {}
878
944
  for key in dependents:
879
945
  full_depends[key] = _get_all_depends(key, dependents)
946
+ if full_depends[key] & after_yield:
947
+ after_yield.add(key)
948
+
949
+ return dependents, full_depends, after_yield
950
+
880
951
 
952
+ def _build_order(description, levels, dependents, full_depends):
881
953
  levels = {}
882
954
  passed = set()
883
955
  all_keys = set(description['consts'].keys())
@@ -922,8 +994,9 @@ def assymbly(description):
922
994
  description['order'][level].append(ready)
923
995
  keys -= set(ready)
924
996
 
997
+
998
+ def _make_axis(description):
925
999
  axis = {}
926
- independent_variables = set(description['intrinsic_loops'].keys())
927
1000
 
928
1001
  for name in description['consts']:
929
1002
  axis[name] = ()
@@ -932,8 +1005,6 @@ def assymbly(description):
932
1005
  if isinstance(iterable, OptimizeSpace):
933
1006
  axis[name] = tuple(range(level + 1))
934
1007
  continue
935
- elif isinstance(iterable, (np.ndarray, list, tuple, range, Space)):
936
- independent_variables.add(name)
937
1008
  axis[name] = (level, )
938
1009
 
939
1010
  for level, group in description['order'].items():
@@ -954,8 +1025,20 @@ def assymbly(description):
954
1025
  k: tuple([x for x in v if x >= 0])
955
1026
  for k, v in axis.items()
956
1027
  }
1028
+
1029
+
1030
+ def assymbly(description):
1031
+ _get_environment(description)
1032
+ levels = _mapping_levels(description)
1033
+ independent_variables = _get_independent_variables(description)
957
1034
  description['independent_variables'] = independent_variables
958
1035
 
1036
+ dependents, full_depends, after_yield = _build_dependents(
1037
+ description, levels, independent_variables)
1038
+
1039
+ _build_order(description, levels, dependents, full_depends)
1040
+ _make_axis(description)
1041
+
959
1042
  return description
960
1043
 
961
1044
 
qulab/scan/server.py CHANGED
@@ -1,6 +1,8 @@
1
1
  import asyncio
2
2
  import os
3
3
  import pickle
4
+ import subprocess
5
+ import sys
4
6
  import time
5
7
  import uuid
6
8
  from pathlib import Path
@@ -18,6 +20,7 @@ from .models import Cell, Notebook
18
20
  from .models import Record as RecordInDB
19
21
  from .models import Session, create_engine, create_tables, sessionmaker, utcnow
20
22
  from .record import BufferList, Record, random_path
23
+ from .utils import dump_dict, load_dict
21
24
 
22
25
  try:
23
26
  default_record_port = int(os.getenv('QULAB_RECORD_PORT', 6789))
@@ -51,6 +54,16 @@ class Request():
51
54
  return f"Request({self.method})"
52
55
 
53
56
 
57
+ class Response():
58
+ pass
59
+
60
+
61
+ class ErrorResponse(Response):
62
+
63
+ def __init__(self, error):
64
+ self.error = error
65
+
66
+
54
67
  async def reply(req, resp):
55
68
  await req.sock.send_multipart([req.identity, pickle.dumps(resp)])
56
69
 
@@ -59,33 +72,45 @@ def clear_cache():
59
72
  if len(record_cache) < CACHE_SIZE:
60
73
  return
61
74
 
75
+ logger.debug(f"clear_cache record_cache: {len(record_cache)}")
62
76
  for k, (t, _) in zip(sorted(record_cache.items(), key=lambda x: x[1][0]),
63
77
  range(len(record_cache) - CACHE_SIZE)):
64
78
  del record_cache[k]
65
79
 
80
+ logger.debug(f"clear_cache buffer_list_cache: {len(buffer_list_cache)}")
66
81
  for k, (t,
67
82
  _) in zip(sorted(buffer_list_cache.items(), key=lambda x: x[1][0]),
68
83
  range(len(buffer_list_cache) - CACHE_SIZE)):
69
84
  del buffer_list_cache[k]
85
+ logger.debug(f"clear_cache done.")
70
86
 
71
87
 
72
88
  def flush_cache():
89
+ logger.debug(f"flush_cache: {len(record_cache)}")
73
90
  for k, (t, r) in record_cache.items():
74
91
  r.flush()
92
+ logger.debug(f"flush_cache done.")
75
93
 
76
94
 
77
95
  def get_local_record(session: Session, id: int, datapath: Path) -> Record:
96
+ logger.debug(f"get_local_record: {id}")
78
97
  record_in_db = session.get(RecordInDB, id)
79
98
  if record_in_db is None:
99
+ logger.debug(f"record not found: {id=}")
80
100
  return None
81
101
  record_in_db.atime = utcnow()
82
102
 
83
103
  if record_in_db.file.endswith('.zip'):
84
- return Record.load(datapath / 'objects' / record_in_db.file)
104
+ logger.debug(f"load record from zip: {record_in_db.file}")
105
+ record = Record.load(datapath / 'objects' / record_in_db.file)
106
+ logger.debug(f"load record from zip done.")
107
+ return record
85
108
 
86
109
  path = datapath / 'objects' / record_in_db.file
87
110
  with open(path, 'rb') as f:
111
+ logger.debug(f"load record from file: {path}")
88
112
  record = dill.load(f)
113
+ logger.debug(f"load record from file done.")
89
114
  record.database = datapath
90
115
  record._file = path
91
116
  return record
@@ -95,13 +120,16 @@ def get_record(session: Session, id: int, datapath: Path) -> Record:
95
120
  if id not in record_cache:
96
121
  record = get_local_record(session, id, datapath)
97
122
  else:
123
+ logger.debug(f"get_record from cache: {id=}")
98
124
  record = record_cache[id][1]
99
125
  clear_cache()
126
+ logger.debug(f"update lru time for record cache: {id=}")
100
127
  record_cache[id] = time.time(), record
101
128
  return record
102
129
 
103
130
 
104
131
  def record_create(session: Session, description: dict, datapath: Path) -> int:
132
+ logger.debug(f"record_create: {description['app']}")
105
133
  record = Record(None, datapath, description)
106
134
  record_in_db = RecordInDB()
107
135
  if 'app' in description:
@@ -111,28 +139,39 @@ def record_create(session: Session, description: dict, datapath: Path) -> int:
111
139
  record_in_db.file = '/'.join(record._file.parts[-4:])
112
140
  record_in_db.config_id = description['config']
113
141
  record._file = datapath / 'objects' / record_in_db.file
142
+ logger.debug(f"record_create generate random file: {record_in_db.file}")
114
143
  session.add(record_in_db)
115
144
  try:
116
145
  session.commit()
146
+ logger.debug(f"record_create commited: record.id={record_in_db.id}")
117
147
  record.id = record_in_db.id
118
148
  clear_cache()
119
149
  record_cache[record.id] = time.time(), record
120
150
  return record.id
121
151
  except:
152
+ logger.debug(f"record_create rollback")
122
153
  session.rollback()
123
154
  raise
124
155
 
125
156
 
126
157
  def record_append(session: Session, record_id: int, level: int, step: int,
127
158
  position: int, variables: dict, datapath: Path):
159
+ logger.debug(f"record_append: {record_id}")
128
160
  record = get_record(session, record_id, datapath)
161
+ logger.debug(f"record_append: {record_id}, {level}, {step}, {position}")
129
162
  record.append(level, step, position, variables)
163
+ logger.debug(f"record_append done.")
130
164
  try:
165
+ logger.debug(f"record_append update SQL database.")
131
166
  record_in_db = session.get(RecordInDB, record_id)
167
+ logger.debug(f"record_append get RecordInDB: {record_in_db}")
132
168
  record_in_db.mtime = utcnow()
133
169
  record_in_db.atime = utcnow()
170
+ logger.debug(f"record_append update RecordInDB: {record_in_db}")
134
171
  session.commit()
172
+ logger.debug(f"record_append commited.")
135
173
  except:
174
+ logger.debug(f"record_append rollback.")
136
175
  session.rollback()
137
176
  raise
138
177
 
@@ -145,16 +184,20 @@ def record_delete(session: Session, record_id: int, datapath: Path):
145
184
  session.commit()
146
185
 
147
186
 
148
- @logger.catch
187
+ @logger.catch(reraise=True)
149
188
  async def handle(session: Session, request: Request, datapath: Path):
150
189
 
151
190
  msg = request.msg
152
191
 
192
+ if request.method not in ['ping']:
193
+ logger.debug(f"handle: {request.method}")
194
+
153
195
  match request.method:
154
196
  case 'ping':
155
197
  await reply(request, 'pong')
156
198
  case 'bufferlist_iter':
157
- if msg['iter_id'] in buffer_list_cache:
199
+ logger.debug(f"bufferlist_iter: {msg}")
200
+ if msg['iter_id'] and msg['iter_id'] in buffer_list_cache:
158
201
  it = buffer_list_cache[msg['iter_id']][1]
159
202
  iter_id = msg['iter_id']
160
203
  else:
@@ -174,22 +217,30 @@ async def handle(session: Session, request: Request, datapath: Path):
174
217
  except StopIteration:
175
218
  end = True
176
219
  break
220
+ logger.debug(f"bufferlist_iter: {iter_id}, {end}")
177
221
  await reply(request, (iter_id, ret, end))
222
+ logger.debug(f"reply bufferlist_iter: {iter_id}, {end}")
178
223
  buffer_list_cache[iter_id] = time.time(), it
179
224
  clear_cache()
180
225
  case 'bufferlist_iter_exit':
226
+ logger.debug(f"bufferlist_iter_exit: {msg}")
181
227
  try:
182
228
  it = buffer_list_cache.pop(msg['iter_id'])[1]
183
229
  it.throw(Exception)
184
230
  except:
185
231
  pass
186
232
  clear_cache()
233
+ logger.debug(f"end bufferlist_iter_exit: {msg}")
187
234
  case 'record_create':
188
- description = dill.loads(msg['description'])
235
+ logger.debug(f"record_create")
236
+ description = load_dict(msg['description'])
189
237
  await reply(request, record_create(session, description, datapath))
238
+ logger.debug(f"reply record_create")
190
239
  case 'record_append':
240
+ logger.debug(f"record_append")
191
241
  record_append(session, msg['record_id'], msg['level'], msg['step'],
192
242
  msg['position'], msg['variables'], datapath)
243
+ logger.debug(f"reply record_append")
193
244
  case 'record_description':
194
245
  record = get_record(session, msg['record_id'], datapath)
195
246
  await reply(request, dill.dumps(record))
@@ -295,6 +346,9 @@ async def handle(session: Session, request: Request, datapath: Path):
295
346
  case _:
296
347
  logger.error(f"Unknown method: {msg['method']}")
297
348
 
349
+ if request.method not in ['ping']:
350
+ logger.debug(f"finished handle: {request.method}")
351
+
298
352
 
299
353
  async def handle_with_timeout(session: Session, request: Request,
300
354
  datapath: Path, timeout: float):
@@ -306,35 +360,45 @@ async def handle_with_timeout(session: Session, request: Request,
306
360
  f"Task handling request {request} timed out and was cancelled.")
307
361
  await reply(request, 'timeout')
308
362
  except Exception as e:
309
- await reply(request, f'{e!r}')
363
+ logger.error(f"Task handling request {request} failed: {e!r}")
364
+ await reply(request, ErrorResponse(f'{e!r}'))
365
+ logger.debug(f"Task handling request {request} finished.")
310
366
 
311
367
 
312
368
  async def serv(port,
313
369
  datapath,
314
- url=None,
370
+ url='',
315
371
  buffer_size=1024 * 1024 * 1024,
316
372
  interval=60):
317
- logger.info('Server starting.')
373
+ logger.debug('Creating socket...')
318
374
  async with ZMQContextManager(zmq.ROUTER, bind=f"tcp://*:{port}") as sock:
319
- if url is None:
375
+ logger.info(f'Server started at port {port}.')
376
+ logger.info(f'Data path: {datapath}.')
377
+ if not url or url == 'sqlite':
320
378
  url = 'sqlite:///' + str(datapath / 'data.db')
321
379
  engine = create_engine(url)
322
380
  create_tables(engine)
323
381
  Session = sessionmaker(engine)
324
382
  with Session() as session:
325
- logger.info('Server started.')
383
+ logger.info(f'Database connected: {url}.')
326
384
  received = 0
327
385
  last_flush_time = time.time()
328
386
  while True:
387
+ logger.debug('Waiting for request...')
329
388
  identity, msg = await sock.recv_multipart()
389
+ logger.debug('Received request.')
330
390
  received += len(msg)
331
391
  try:
332
392
  req = Request(sock, identity, msg)
333
- except:
393
+ except Exception as e:
334
394
  logger.exception('bad request')
395
+ await sock.send_multipart(
396
+ [identity,
397
+ pickle.dumps(ErrorResponse(f'{e!r}'))])
335
398
  continue
336
399
  asyncio.create_task(
337
- handle_with_timeout(session, req, datapath, timeout=60.0))
400
+ handle_with_timeout(session, req, datapath,
401
+ timeout=3600.0))
338
402
  if received > buffer_size or time.time(
339
403
  ) - last_flush_time > interval:
340
404
  flush_cache()
@@ -342,46 +406,123 @@ async def serv(port,
342
406
  last_flush_time = time.time()
343
407
 
344
408
 
345
- async def watch(port, datapath, url=None, timeout=1, buffer=1024, interval=60):
346
- with ZMQContextManager(zmq.DEALER,
347
- connect=f"tcp://127.0.0.1:{port}") as sock:
348
- sock.setsockopt(zmq.LINGER, 0)
409
+ async def main(port,
410
+ datapath,
411
+ url,
412
+ timeout=1,
413
+ buffer=1024,
414
+ interval=60,
415
+ log='stderr',
416
+ no_watch=True,
417
+ debug=False):
418
+ if no_watch:
419
+ logger.remove()
420
+ if debug:
421
+ level = 'DEBUG'
422
+ else:
423
+ level = 'INFO'
424
+ if log == 'stderr':
425
+ logger.add(sys.stderr, level=level)
426
+ elif log == 'stdout':
427
+ logger.add(sys.stdout, level=level)
428
+ else:
429
+ logger.add(sys.stderr, level=level)
430
+ logger.add(log, level=level)
431
+ logger.debug(f"logging level: {level}")
432
+ logger.info('Server starting...')
433
+ await serv(port, datapath, url, buffer * 1024 * 1024, interval)
434
+ else:
435
+ process = None
436
+
349
437
  while True:
350
438
  try:
351
- sock.send_pyobj({"method": "ping"})
352
- if sock.poll(int(1000 * timeout)):
353
- sock.recv()
354
- else:
355
- raise asyncio.TimeoutError()
439
+ with ZMQContextManager(
440
+ zmq.DEALER, connect=f"tcp://127.0.0.1:{port}") as sock:
441
+ sock.setsockopt(zmq.LINGER, 0)
442
+ sock.send_pyobj({"method": "ping"})
443
+ logger.debug('ping.')
444
+ if sock.poll(int(1000 * timeout)):
445
+ sock.recv()
446
+ logger.debug('recv pong.')
447
+ else:
448
+ logger.debug('timeout.')
449
+ raise asyncio.TimeoutError()
356
450
  except (zmq.error.ZMQError, asyncio.TimeoutError):
357
- return asyncio.create_task(
358
- serv(port, datapath, url, buffer * 1024 * 1024, interval))
451
+ if process is not None:
452
+ logger.debug(
453
+ f'killing process... PID={process.pid}, returncode={process.returncode}'
454
+ )
455
+ process.kill()
456
+ logger.debug(
457
+ f'killed process. PID={process.pid}, returncode={process.returncode}'
458
+ )
459
+ cmd = [
460
+ sys.executable,
461
+ "-m",
462
+ "qulab",
463
+ "server",
464
+ "--port",
465
+ f"{port}",
466
+ "--datapath",
467
+ f"{datapath}",
468
+ "--url",
469
+ f"{url}",
470
+ "--timeout",
471
+ f"{timeout}",
472
+ "--buffer",
473
+ f"{buffer}",
474
+ "--interval",
475
+ f"{interval}",
476
+ "--log",
477
+ f"{log}",
478
+ ]
479
+ if url:
480
+ cmd.extend(['--url', url])
481
+ if debug:
482
+ cmd.append('--debug')
483
+ cmd.append("--no-watch")
484
+ logger.debug(f"starting process: {' '.join(cmd)}")
485
+ process = subprocess.Popen(cmd, cwd=os.getcwd())
486
+ logger.debug(
487
+ f'process started. PID={process.pid}, returncode={process.returncode}'
488
+ )
489
+
490
+ # Capture and log the output
491
+ # stdout, stderr = process.communicate(timeout=5)
492
+ # if stdout:
493
+ # logger.info(f'Server stdout: {stdout.decode()}')
494
+ # if stderr:
495
+ # logger.error(f'Server stderr: {stderr.decode()}')
496
+
497
+ await asyncio.sleep(5)
359
498
  await asyncio.sleep(timeout)
360
499
 
361
500
 
362
- async def main(port, datapath, url, timeout=1, buffer=1024, interval=60):
363
- task = await watch(port=port,
364
- datapath=datapath,
365
- url=url,
366
- timeout=timeout,
367
- buffer=buffer,
368
- interval=interval)
369
- await task
370
-
371
-
372
501
  @click.command()
373
502
  @click.option('--port',
374
503
  default=os.getenv('QULAB_RECORD_PORT', 6789),
375
504
  help='Port of the server.')
376
505
  @click.option('--datapath', default=datapath, help='Path of the data.')
377
- @click.option('--url', default=None, help='URL of the database.')
506
+ @click.option('--url', default='sqlite', help='URL of the database.')
378
507
  @click.option('--timeout', default=1, help='Timeout of ping.')
379
508
  @click.option('--buffer', default=1024, help='Buffer size (MB).')
380
509
  @click.option('--interval',
381
510
  default=60,
382
511
  help='Interval of flush cache, in unit of second.')
383
- def server(port, datapath, url, timeout, buffer, interval):
384
- asyncio.run(main(port, Path(datapath), url, timeout, buffer, interval))
512
+ @click.option('--log', default='stderr', help='Log file.')
513
+ @click.option('--no-watch', is_flag=True, help='Watch the server.')
514
+ @click.option('--debug', is_flag=True, help='Debug mode.')
515
+ def server(port, datapath, url, timeout, buffer, interval, log, no_watch,
516
+ debug):
517
+ try:
518
+ import uvloop
519
+ uvloop.run(
520
+ main(port, Path(datapath), url, timeout, buffer, interval, log,
521
+ True, debug))
522
+ except ImportError:
523
+ asyncio.run(
524
+ main(port, Path(datapath), url, timeout, buffer, interval, log,
525
+ True, debug))
385
526
 
386
527
 
387
528
  if __name__ == "__main__":
qulab/scan/utils.py CHANGED
@@ -29,6 +29,46 @@ class TooLarge:
29
29
  return f'<TooLarge: {self.type} at 0x{id(self):x}>'
30
30
 
31
31
 
32
+ def dump_dict(d, keys=[]):
33
+ ret = {}
34
+
35
+ for key, value in d.items():
36
+ if key in keys:
37
+ ret[key] = value
38
+ continue
39
+ if isinstance(value, dict) and isinstance(key, str):
40
+ ret[key] = dump_dict(value,
41
+ keys=[
42
+ k[len(key) + 1:] for k in keys
43
+ if k.startswith(f'{key}.')
44
+ ])
45
+ else:
46
+ try:
47
+ ret[key] = dill.dumps(value)
48
+ except:
49
+ ret[key] = Unpicklable(value)
50
+
51
+ return dill.dumps(ret)
52
+
53
+
54
+ def load_dict(buff):
55
+ if isinstance(buff, dict):
56
+ return {key: load_dict(value) for key, value in buff.items()}
57
+
58
+ if not isinstance(buff, bytes):
59
+ return buff
60
+
61
+ try:
62
+ ret = dill.loads(buff)
63
+ except:
64
+ return buff
65
+
66
+ if isinstance(ret, dict):
67
+ return load_dict(ret)
68
+ else:
69
+ return ret
70
+
71
+
32
72
  def dump_globals(ns=None, *, size_limit=10 * 1024 * 1024, warn=False):
33
73
  import __main__
34
74
 
qulab/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "2.3.2"
1
+ __version__ = "2.3.4"
File without changes
File without changes