asyncmd 0.3.2__py3-none-any.whl → 0.3.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.
- asyncmd/gromacs/mdconfig.py +1 -1
- asyncmd/gromacs/mdengine.py +29 -13
- asyncmd/gromacs/utils.py +6 -6
- asyncmd/mdconfig.py +2 -2
- asyncmd/slurm.py +191 -41
- asyncmd/trajectory/functionwrapper.py +23 -7
- asyncmd/trajectory/propagate.py +10 -9
- asyncmd/trajectory/trajectory.py +6 -6
- {asyncmd-0.3.2.dist-info → asyncmd-0.3.3.dist-info}/METADATA +19 -4
- asyncmd-0.3.3.dist-info/RECORD +23 -0
- {asyncmd-0.3.2.dist-info → asyncmd-0.3.3.dist-info}/WHEEL +1 -1
- asyncmd-0.3.2.dist-info/RECORD +0 -23
- {asyncmd-0.3.2.dist-info → asyncmd-0.3.3.dist-info/licenses}/LICENSE +0 -0
- {asyncmd-0.3.2.dist-info → asyncmd-0.3.3.dist-info}/top_level.txt +0 -0
asyncmd/gromacs/mdconfig.py
CHANGED
@@ -302,7 +302,7 @@ class MDP(LineBasedMDConfig):
|
|
302
302
|
# option 2 (and 3 if the comment is before the equal sign)
|
303
303
|
# comment sign is the first letter, so the whole line is
|
304
304
|
# (most probably) a comment line
|
305
|
-
logger.debug(
|
305
|
+
logger.debug("mdp line parsed as comment: %s", line)
|
306
306
|
return {}
|
307
307
|
if ((len(splits_at_equal) == 2 and len(splits_at_comment) == 1) # option 1
|
308
308
|
# or option 3 with equal sign before comment sign
|
asyncmd/gromacs/mdengine.py
CHANGED
@@ -626,8 +626,8 @@ class GmxEngine(MDEngine):
|
|
626
626
|
+ f"{cpt_fname} does not exist."
|
627
627
|
)
|
628
628
|
starting_configuration = cpt_fname
|
629
|
-
logger.warning("Starting value for 'simulation-part' > 1 (=%s)"
|
630
|
-
"
|
629
|
+
logger.warning("Starting value for 'simulation-part' > 1 (=%s) "
|
630
|
+
"and existing checkpoint file found (%s). "
|
631
631
|
"Using the checkpoint file as "
|
632
632
|
"`starting_configuration`.",
|
633
633
|
sim_part, cpt_fname)
|
@@ -712,7 +712,7 @@ class GmxEngine(MDEngine):
|
|
712
712
|
stdout, stderr = await grompp_proc.communicate()
|
713
713
|
return_code = grompp_proc.returncode
|
714
714
|
logger.debug("gmx grompp command returned return code %s.",
|
715
|
-
return_code)
|
715
|
+
str(return_code) if return_code is not None else "not available")
|
716
716
|
#logger.debug("grompp stdout:\n%s", stdout.decode())
|
717
717
|
#logger.debug("grompp stderr:\n%s", stderr.decode())
|
718
718
|
if return_code != 0:
|
@@ -754,9 +754,9 @@ class GmxEngine(MDEngine):
|
|
754
754
|
last_partnum = int(last_trajname[len(deffnm) + 5:len(deffnm) + 9])
|
755
755
|
if last_partnum != len(previous_trajs):
|
756
756
|
logger.warning("Not all previous trajectory parts seem to be "
|
757
|
-
|
758
|
-
|
759
|
-
|
757
|
+
"present in the current workdir. Assuming the "
|
758
|
+
"highest part number corresponds to the "
|
759
|
+
"checkpoint file and continuing anyway."
|
760
760
|
)
|
761
761
|
# load the 'old' mdp_in
|
762
762
|
async with _SEMAPHORES["MAX_FILES_OPEN"]:
|
@@ -869,7 +869,7 @@ class GmxEngine(MDEngine):
|
|
869
869
|
raise e from None # reraise the error for encompassing coroutines
|
870
870
|
else:
|
871
871
|
logger.debug("gmx mdrun command returned return code %s.",
|
872
|
-
returncode)
|
872
|
+
str(returncode) if returncode is not None else "not available")
|
873
873
|
#logger.debug("gmx mdrun stdout:\n%s", stdout.decode())
|
874
874
|
#logger.debug("gmx mdrun stderr:\n%s", stderr.decode())
|
875
875
|
if returncode == 0:
|
@@ -1025,6 +1025,7 @@ class SlurmGmxEngine(GmxEngine):
|
|
1025
1025
|
# although the mdrun was successfull.
|
1026
1026
|
|
1027
1027
|
def __init__(self, mdconfig, gro_file, top_file, sbatch_script, ndx_file=None,
|
1028
|
+
sbatch_options: dict | None = None,
|
1028
1029
|
**kwargs):
|
1029
1030
|
"""
|
1030
1031
|
Initialize a :class:`SlurmGmxEngine`.
|
@@ -1047,6 +1048,19 @@ class SlurmGmxEngine(GmxEngine):
|
|
1047
1048
|
|
1048
1049
|
ndx_file: str or None
|
1049
1050
|
Optional, absolute or relative path to a gromacs index file.
|
1051
|
+
sbatch_options : dict or None
|
1052
|
+
Dictionary of sbatch options, keys are long names for options,
|
1053
|
+
values are the correponding values. The keys/long names are given
|
1054
|
+
without the dashes, e.g. to specify "--mem=1024" the dictionary
|
1055
|
+
needs to be {"mem": "1024"}. To specify options without values use
|
1056
|
+
keys with empty strings as values, e.g. to specify "--contiguous"
|
1057
|
+
the dictionary needs to be {"contiguous": ""}.
|
1058
|
+
See the SLURM documentation for a full list of sbatch options
|
1059
|
+
(https://slurm.schedmd.com/sbatch.html).
|
1060
|
+
Note: This argument is passed as is to the `SlurmProcess` in which
|
1061
|
+
the computation is performed. Each call to the engines `run` method
|
1062
|
+
triggers the creation of a new `SlurmProcess` and will use the then
|
1063
|
+
current `sbatch_options`.
|
1050
1064
|
|
1051
1065
|
Note that all attributes can be set at intialization by passing keyword
|
1052
1066
|
arguments with their name, e.g. mdrun_extra_args="-ntomp 2" to instruct
|
@@ -1063,6 +1077,7 @@ class SlurmGmxEngine(GmxEngine):
|
|
1063
1077
|
with open(sbatch_script, 'r') as f:
|
1064
1078
|
sbatch_script = f.read()
|
1065
1079
|
self.sbatch_script = sbatch_script
|
1080
|
+
self.sbatch_options = sbatch_options
|
1066
1081
|
|
1067
1082
|
def _name_from_name_or_none(self, run_name: typing.Optional[str]) -> str:
|
1068
1083
|
if run_name is not None:
|
@@ -1089,12 +1104,13 @@ class SlurmGmxEngine(GmxEngine):
|
|
1089
1104
|
async with aiofiles.open(fname, 'w') as f:
|
1090
1105
|
await f.write(script)
|
1091
1106
|
self._proc = await slurm.create_slurmprocess_submit(
|
1092
|
-
|
1093
|
-
|
1094
|
-
|
1095
|
-
|
1096
|
-
|
1097
|
-
|
1107
|
+
jobname=name,
|
1108
|
+
sbatch_script=fname,
|
1109
|
+
workdir=workdir,
|
1110
|
+
time=walltime,
|
1111
|
+
sbatch_options=self.sbatch_options,
|
1112
|
+
stdfiles_removal="success",
|
1113
|
+
stdin=None,
|
1098
1114
|
)
|
1099
1115
|
|
1100
1116
|
async def _acquire_resources_gmx_mdrun(self, **kwargs):
|
asyncmd/gromacs/utils.py
CHANGED
@@ -172,12 +172,12 @@ def ensure_mdp_options(mdp: MDP, genvel: str = "no", continuation: str = "yes")
|
|
172
172
|
# make sure we do not generate velocities with gromacs
|
173
173
|
genvel_test = mdp["gen-vel"]
|
174
174
|
except KeyError:
|
175
|
-
logger.info(
|
175
|
+
logger.info("Setting 'gen-vel = %s' in mdp.", genvel)
|
176
176
|
mdp["gen-vel"] = genvel
|
177
177
|
else:
|
178
178
|
if genvel_test != genvel:
|
179
|
-
logger.warning(
|
180
|
-
|
179
|
+
logger.warning("Setting 'gen-vel = %s' in mdp "
|
180
|
+
"(was '%s').", genvel, genvel_test)
|
181
181
|
mdp["gen-vel"] = genvel
|
182
182
|
try:
|
183
183
|
# TODO/FIXME: this could also be 'unconstrained-start'!
|
@@ -186,12 +186,12 @@ def ensure_mdp_options(mdp: MDP, genvel: str = "no", continuation: str = "yes")
|
|
186
186
|
# so I think we can ignore that for now?!
|
187
187
|
continuation_test = mdp["continuation"]
|
188
188
|
except KeyError:
|
189
|
-
logger.info(
|
189
|
+
logger.info("Setting 'continuation = %s' in mdp.", continuation)
|
190
190
|
mdp["continuation"] = continuation
|
191
191
|
else:
|
192
192
|
if continuation_test != continuation:
|
193
|
-
logger.warning(
|
194
|
-
|
193
|
+
logger.warning("Setting 'continuation = %s' in mdp "
|
194
|
+
"(was '%s').", continuation, continuation_test)
|
195
195
|
mdp["continuation"] = continuation
|
196
196
|
|
197
197
|
return mdp
|
asyncmd/mdconfig.py
CHANGED
@@ -386,8 +386,8 @@ class LineBasedMDConfig(MDConfig):
|
|
386
386
|
else:
|
387
387
|
# warn that we will only keep the last occurenc of key
|
388
388
|
logger.warning("Parsed duplicate configuration option "
|
389
|
-
|
390
|
-
|
389
|
+
"(%s). Last values encountered take "
|
390
|
+
"precedence.", key)
|
391
391
|
parsed.update(line_parsed)
|
392
392
|
# convert the known types
|
393
393
|
self._config = {key: self._type_dispatch[key](value)
|
asyncmd/slurm.py
CHANGED
@@ -130,7 +130,7 @@ class SlurmClusterMediator:
|
|
130
130
|
# (here forever means until we reinitialize SlurmClusterMediator)
|
131
131
|
|
132
132
|
def __init__(self, **kwargs) -> None:
|
133
|
-
self._exclude_nodes = []
|
133
|
+
self._exclude_nodes: list[str] = []
|
134
134
|
# make it possible to set any attribute via kwargs
|
135
135
|
# check the type for attributes with default values
|
136
136
|
dval = object()
|
@@ -151,17 +151,17 @@ class SlurmClusterMediator:
|
|
151
151
|
# this either checks for our defaults or whatever we just set via kwargs
|
152
152
|
self.sacct_executable = ensure_executable_available(self.sacct_executable)
|
153
153
|
self.sinfo_executable = ensure_executable_available(self.sinfo_executable)
|
154
|
-
self._node_job_fails = collections.Counter()
|
155
|
-
self._node_job_successes = collections.Counter()
|
154
|
+
self._node_job_fails: dict[str,int] = collections.Counter()
|
155
|
+
self._node_job_successes: dict[str,int] = collections.Counter()
|
156
156
|
self._all_nodes = self.list_all_nodes()
|
157
|
-
self._jobids = [] # list of jobids of jobs we know about
|
158
|
-
self._jobids_sacct = [] # list of jobids we monitor actively via sacct
|
157
|
+
self._jobids: list[str] = [] # list of jobids of jobs we know about
|
158
|
+
self._jobids_sacct: list[str] = [] # list of jobids we monitor actively via sacct
|
159
159
|
# we will store the info about jobs in a dict keys are jobids
|
160
160
|
# values are dicts with key queried option and value the (parsed)
|
161
161
|
# return value
|
162
162
|
# currently queried options are: state, exitcode and nodelist
|
163
|
-
self._jobinfo = {}
|
164
|
-
self._last_sacct_call = 0 # make sure we dont call sacct too often
|
163
|
+
self._jobinfo: dict[str,dict] = {}
|
164
|
+
self._last_sacct_call = 0. # make sure we dont call sacct too often
|
165
165
|
# make sure we can only call sacct once at a time
|
166
166
|
# (since there is only one ClusterMediator at a time we can create
|
167
167
|
# the semaphore here in __init__)
|
@@ -383,17 +383,18 @@ class SlurmClusterMediator:
|
|
383
383
|
self._jobinfo[jobid]["nodelist"] = nodelist
|
384
384
|
self._jobinfo[jobid]["exitcode"] = exitcode
|
385
385
|
self._jobinfo[jobid]["state"] = state
|
386
|
-
logger.debug(
|
387
|
-
|
388
|
-
|
386
|
+
logger.debug("Extracted from sacct output: jobid %s, state %s, "
|
387
|
+
"exitcode %s and nodelist %s.",
|
388
|
+
jobid, state, exitcode, nodelist,
|
389
|
+
)
|
389
390
|
parsed_ec = self._parse_exitcode_from_slurm_state(slurm_state=state)
|
390
391
|
self._jobinfo[jobid]["parsed_exitcode"] = parsed_ec
|
391
392
|
if parsed_ec is not None:
|
392
|
-
logger.debug("Parsed slurm state %s for job %s"
|
393
|
-
"
|
394
|
-
"
|
395
|
-
|
396
|
-
|
393
|
+
logger.debug("Parsed slurm state %s for job %s as "
|
394
|
+
"returncode %s. Removing job from sacct calls "
|
395
|
+
"because its state will not change anymore.",
|
396
|
+
state, jobid, str(parsed_ec) if parsed_ec is not None
|
397
|
+
else "not available",
|
397
398
|
)
|
398
399
|
self._jobids_sacct.remove(jobid)
|
399
400
|
self._node_fail_heuristic(jobid=jobid,
|
@@ -434,8 +435,8 @@ class SlurmClusterMediator:
|
|
434
435
|
# make the string a list of single node hostnames
|
435
436
|
hostnameprefix, nums = nodelist.split("[")
|
436
437
|
nums = nums.rstrip("]")
|
437
|
-
|
438
|
-
return [f"{hostnameprefix}{num}" for num in
|
438
|
+
nums_list = nums.split(",")
|
439
|
+
return [f"{hostnameprefix}{num}" for num in nums_list]
|
439
440
|
|
440
441
|
def _parse_exitcode_from_slurm_state(self,
|
441
442
|
slurm_state: str,
|
@@ -443,8 +444,9 @@ class SlurmClusterMediator:
|
|
443
444
|
for ecode, regexp in self._ecode_for_slurmstate_regexps.items():
|
444
445
|
if regexp.search(slurm_state):
|
445
446
|
# regexp matches the given slurm_state
|
446
|
-
logger.debug("Parsed SLURM state %s as exitcode %
|
447
|
-
slurm_state, ecode
|
447
|
+
logger.debug("Parsed SLURM state %s as exitcode %s.",
|
448
|
+
slurm_state, str(ecode) if ecode is not None
|
449
|
+
else "not available",
|
448
450
|
)
|
449
451
|
return ecode
|
450
452
|
# we should never finish the loop, it means we miss a slurm job state
|
@@ -522,11 +524,11 @@ class SlurmClusterMediator:
|
|
522
524
|
all_nodes = len(self._all_nodes)
|
523
525
|
exclude_nodes = len(self._exclude_nodes)
|
524
526
|
if exclude_nodes >= all_nodes / 4:
|
525
|
-
logger.error("We already declared 1/4 of the cluster as broken."
|
526
|
-
|
527
|
+
logger.error("We already declared 1/4 of the cluster as broken. "
|
528
|
+
"Houston, we might have a problem?")
|
527
529
|
if exclude_nodes >= all_nodes / 2:
|
528
|
-
logger.error("In fact we declared 1/2 of the cluster as broken."
|
529
|
-
|
530
|
+
logger.error("In fact we declared 1/2 of the cluster as broken. "
|
531
|
+
"Houston, we *do* have a problem!")
|
530
532
|
if exclude_nodes >= all_nodes * 0.75:
|
531
533
|
raise RuntimeError("Houston? 3/4 of the cluster is broken?")
|
532
534
|
|
@@ -581,9 +583,9 @@ class SlurmProcess:
|
|
581
583
|
_slurm_cluster_mediator = None
|
582
584
|
# we raise a ValueError if sacct/sinfo are not available
|
583
585
|
logger.warning("Could not initialize SLURM cluster handling. "
|
584
|
-
"If you are sure SLURM (sinfo/sacct/etc) is available"
|
585
|
-
"
|
586
|
-
"
|
586
|
+
"If you are sure SLURM (sinfo/sacct/etc) is available "
|
587
|
+
"try calling `asyncmd.config.set_slurm_settings()` "
|
588
|
+
"with the appropriate arguments.")
|
587
589
|
# we can not simply wait for the subprocess, since slurm exits directly
|
588
590
|
# so we will sleep for this long between checks if slurm-job completed
|
589
591
|
sleep_time = 15 # TODO: heuristic? dynamically adapt?
|
@@ -597,8 +599,9 @@ class SlurmProcess:
|
|
597
599
|
scancel_executable = "scancel"
|
598
600
|
|
599
601
|
def __init__(self, jobname: str, sbatch_script: str,
|
600
|
-
workdir:
|
601
|
-
time:
|
602
|
+
workdir: str | None = None,
|
603
|
+
time: float | None = None,
|
604
|
+
sbatch_options: dict | None = None,
|
602
605
|
stdfiles_removal: str = "success",
|
603
606
|
**kwargs) -> None:
|
604
607
|
"""
|
@@ -619,6 +622,15 @@ class SlurmProcess:
|
|
619
622
|
time : float or None
|
620
623
|
Timelimit for the job in hours. None will result in using the
|
621
624
|
default as either specified in the sbatch script or the partition.
|
625
|
+
sbatch_options : dict or None
|
626
|
+
Dictionary of sbatch options, keys are long names for options,
|
627
|
+
values are the correponding values. The keys/long names are given
|
628
|
+
without the dashes, e.g. to specify "--mem=1024" the dictionary
|
629
|
+
needs to be {"mem": "1024"}. To specify options without values use
|
630
|
+
keys with empty strings as values, e.g. to specify "--contiguous"
|
631
|
+
the dictionary needs to be {"contiguous": ""}.
|
632
|
+
See the SLURM documentation for a full list of sbatch options
|
633
|
+
(https://slurm.schedmd.com/sbatch.html).
|
622
634
|
stdfiles_removal : str
|
623
635
|
Whether to remove the stdout, stderr (and possibly stdin) files.
|
624
636
|
Possible values are:
|
@@ -664,13 +676,136 @@ class SlurmProcess:
|
|
664
676
|
if workdir is None:
|
665
677
|
workdir = os.getcwd()
|
666
678
|
self.workdir = os.path.abspath(workdir)
|
667
|
-
self.
|
679
|
+
self._time = time
|
680
|
+
# Use the property to directly call _sanitize_sbatch_options when assigning
|
681
|
+
# Do this **after** setting self._time to ensure consistency
|
682
|
+
if sbatch_options is None:
|
683
|
+
sbatch_options = {}
|
684
|
+
self.sbatch_options = sbatch_options
|
668
685
|
self.stdfiles_removal = stdfiles_removal
|
669
|
-
self._jobid = None
|
670
|
-
|
671
|
-
self.
|
672
|
-
self.
|
673
|
-
self.
|
686
|
+
self._jobid: None | str = None
|
687
|
+
# dict with jobinfo cached from slurm cluster mediator
|
688
|
+
self._jobinfo: dict[str,typing.Any] = {}
|
689
|
+
self._stdout_data: None | bytes = None
|
690
|
+
self._stderr_data: None | bytes = None
|
691
|
+
self._stdin: None | str = None
|
692
|
+
|
693
|
+
def _sanitize_sbatch_options(self, sbatch_options: dict) -> dict:
|
694
|
+
"""
|
695
|
+
Return sane sbatch_options dictionary to be consistent (with self).
|
696
|
+
|
697
|
+
Parameters
|
698
|
+
----------
|
699
|
+
sbatch_options : dict
|
700
|
+
Dictionary of sbatch options.
|
701
|
+
|
702
|
+
Returns
|
703
|
+
-------
|
704
|
+
dict
|
705
|
+
Dictionary with sanitized sbatch options.
|
706
|
+
"""
|
707
|
+
# NOTE: this should be called every time we modify sbatch_options or self.time!
|
708
|
+
# This is the list of sbatch options we use ourself, they should not
|
709
|
+
# be in the dict to avoid unforseen effects. We treat 'time' special
|
710
|
+
# because we want to allow for it to be specified via sbtach_options if
|
711
|
+
# it is not set via the attribute self.time.
|
712
|
+
reserved_sbatch_options = ["job-name", "chdir", "output", "error",
|
713
|
+
"input", "exclude", "parsable"]
|
714
|
+
new_sbatch_options = sbatch_options.copy()
|
715
|
+
if "time" in sbatch_options:
|
716
|
+
if self._time is not None:
|
717
|
+
logger.warning("Removing sbatch option 'time' from 'sbatch_options'. "
|
718
|
+
"Using the 'time' argument instead.")
|
719
|
+
del new_sbatch_options["time"]
|
720
|
+
else:
|
721
|
+
logger.debug("Using 'time' from 'sbatch_options' because "
|
722
|
+
"self.time is None.")
|
723
|
+
for option in reserved_sbatch_options:
|
724
|
+
if option in sbatch_options:
|
725
|
+
logger.warning("Removing sbatch option '%s' from "
|
726
|
+
"'sbatch_options' because it is used internaly "
|
727
|
+
"by the `SlurmProcess`.", option)
|
728
|
+
del new_sbatch_options[option]
|
729
|
+
|
730
|
+
return new_sbatch_options
|
731
|
+
|
732
|
+
def _slurm_timelimit_from_time_in_hours(self, time: float) -> str:
|
733
|
+
"""
|
734
|
+
Create timelimit in SLURM compatible format from time in hours.
|
735
|
+
|
736
|
+
Parameters
|
737
|
+
----------
|
738
|
+
timelimit : float
|
739
|
+
Timelimit for job in hours
|
740
|
+
|
741
|
+
Returns
|
742
|
+
-------
|
743
|
+
str
|
744
|
+
Timelimit for job as SLURM compatible string.
|
745
|
+
"""
|
746
|
+
timelimit = time * 60
|
747
|
+
timelimit_min = int(timelimit) # take only the full minutes
|
748
|
+
timelimit_sec = round(60 * (timelimit - timelimit_min))
|
749
|
+
timelimit_str = f"{timelimit_min}:{timelimit_sec}"
|
750
|
+
return timelimit_str
|
751
|
+
|
752
|
+
@property
|
753
|
+
def time(self) -> float | None:
|
754
|
+
"""
|
755
|
+
Timelimit for SLURM job in hours.
|
756
|
+
|
757
|
+
Can be a float or None (meaning do not specify a timelimit).
|
758
|
+
"""
|
759
|
+
return self._time
|
760
|
+
|
761
|
+
@time.setter
|
762
|
+
def time(self, val: float | None) -> None:
|
763
|
+
self._time = val
|
764
|
+
self._sbatch_options: dict = self._sanitize_sbatch_options(self._sbatch_options)
|
765
|
+
|
766
|
+
@property
|
767
|
+
def sbatch_options(self) -> dict:
|
768
|
+
"""
|
769
|
+
A copy of the sbatch_options dictionary.
|
770
|
+
|
771
|
+
Note that modifying single key, value pairs has no effect, to modify
|
772
|
+
(single) sbatch_options either use the `set_sbatch_option` and
|
773
|
+
`del_sbatch_option` methods or (re)assign a dictionary to
|
774
|
+
`sbatch_options`.
|
775
|
+
"""
|
776
|
+
return self._sbatch_options.copy()
|
777
|
+
|
778
|
+
@sbatch_options.setter
|
779
|
+
def sbatch_options(self, val: dict) -> None:
|
780
|
+
self._sbatch_options = self._sanitize_sbatch_options(val)
|
781
|
+
|
782
|
+
def set_sbatch_option(self, key: str, value: str) -> None:
|
783
|
+
"""
|
784
|
+
Set sbatch option with given key to value.
|
785
|
+
|
786
|
+
I.e. add/modify single key, value pair in sbatch_options.
|
787
|
+
|
788
|
+
Parameters
|
789
|
+
----------
|
790
|
+
key : str
|
791
|
+
The name of the sbatch option.
|
792
|
+
value : str
|
793
|
+
The value for the sbatch option.
|
794
|
+
"""
|
795
|
+
self._sbatch_options[key] = value
|
796
|
+
self._sbatch_options = self._sanitize_sbatch_options(self._sbatch_options)
|
797
|
+
|
798
|
+
def del_sbatch_option(self, key: str) -> None:
|
799
|
+
"""
|
800
|
+
Delete sbatch option with given key from sbatch_options.
|
801
|
+
|
802
|
+
Parameters
|
803
|
+
----------
|
804
|
+
key : str
|
805
|
+
The name of the sbatch option to delete.
|
806
|
+
"""
|
807
|
+
del self._sbatch_options[key]
|
808
|
+
self._sbatch_options = self._sanitize_sbatch_options(self._sbatch_options)
|
674
809
|
|
675
810
|
@property
|
676
811
|
def stdfiles_removal(self) -> str:
|
@@ -734,10 +869,7 @@ class SlurmProcess:
|
|
734
869
|
sbatch_cmd += f" --output=./{self._stdout_name(use_slurm_symbols=True)}"
|
735
870
|
sbatch_cmd += f" --error=./{self._stderr_name(use_slurm_symbols=True)}"
|
736
871
|
if self.time is not None:
|
737
|
-
|
738
|
-
timelimit_min = int(timelimit) # take only the full minutes
|
739
|
-
timelimit_sec = round(60 * (timelimit - timelimit_min))
|
740
|
-
timelimit_str = f"{timelimit_min}:{timelimit_sec}"
|
872
|
+
timelimit_str = self._slurm_timelimit_from_time_in_hours(self.time)
|
741
873
|
sbatch_cmd += f" --time={timelimit_str}"
|
742
874
|
# keep a ref to the stdin value, we need it in communicate
|
743
875
|
self._stdin = stdin
|
@@ -749,6 +881,11 @@ class SlurmProcess:
|
|
749
881
|
exclude_nodes = self.slurm_cluster_mediator.exclude_nodes
|
750
882
|
if len(exclude_nodes) > 0:
|
751
883
|
sbatch_cmd += f" --exclude={','.join(exclude_nodes)}"
|
884
|
+
for key, val in self.sbatch_options.items():
|
885
|
+
if val == "":
|
886
|
+
sbatch_cmd += f" --{key}"
|
887
|
+
else:
|
888
|
+
sbatch_cmd += f" --{key}={val}"
|
752
889
|
sbatch_cmd += f" --parsable {self.sbatch_script}"
|
753
890
|
logger.debug("About to execute sbatch_cmd %s.", sbatch_cmd)
|
754
891
|
# 3 file descriptors: stdin,stdout,stderr
|
@@ -908,7 +1045,7 @@ class SlurmProcess:
|
|
908
1045
|
RuntimeError
|
909
1046
|
If the job has never been submitted.
|
910
1047
|
"""
|
911
|
-
if self.
|
1048
|
+
if self.slurm_jobid is None:
|
912
1049
|
# make sure we can only wait after submitting, otherwise we would
|
913
1050
|
# wait indefinitively if we call wait() before submit()
|
914
1051
|
raise RuntimeError("Can only wait for submitted SLURM jobs with "
|
@@ -1012,8 +1149,10 @@ class SlurmProcess:
|
|
1012
1149
|
+ f" and output {e.output}."
|
1013
1150
|
) from e
|
1014
1151
|
# if we got until here the job is successfuly canceled....
|
1015
|
-
logger.debug(
|
1016
|
-
|
1152
|
+
logger.debug("Canceled SLURM job with jobid %s. "
|
1153
|
+
"scancel returned %s.",
|
1154
|
+
self.slurm_jobid, scancel_out,
|
1155
|
+
)
|
1017
1156
|
# remove the job from the monitoring
|
1018
1157
|
self.slurm_cluster_mediator.monitor_remove_job(jobid=self._jobid)
|
1019
1158
|
if (self._stdfiles_removal == "yes"
|
@@ -1034,6 +1173,7 @@ async def create_slurmprocess_submit(jobname: str,
|
|
1034
1173
|
sbatch_script: str,
|
1035
1174
|
workdir: str,
|
1036
1175
|
time: typing.Optional[float] = None,
|
1176
|
+
sbatch_options: dict | None = None,
|
1037
1177
|
stdfiles_removal: str = "success",
|
1038
1178
|
stdin: typing.Optional[str] = None,
|
1039
1179
|
**kwargs,
|
@@ -1055,6 +1195,15 @@ async def create_slurmprocess_submit(jobname: str,
|
|
1055
1195
|
time : float or None
|
1056
1196
|
Timelimit for the job in hours. None will result in using the
|
1057
1197
|
default as either specified in the sbatch script or the partition.
|
1198
|
+
sbatch_options : dict or None
|
1199
|
+
Dictionary of sbatch options, keys are long names for options,
|
1200
|
+
values are the correponding values. The keys/long names are given
|
1201
|
+
without the dashes, e.g. to specify "--mem=1024" the dictionary
|
1202
|
+
needs to be {"mem": "1024"}. To specify options without values use
|
1203
|
+
keys with empty strings as values, e.g. to specify "--contiguous"
|
1204
|
+
the dictionary needs to be {"contiguous": ""}.
|
1205
|
+
See the SLURM documentation for a full list of sbatch options
|
1206
|
+
(https://slurm.schedmd.com/sbatch.html).
|
1058
1207
|
stdfiles_removal : str
|
1059
1208
|
Whether to remove the stdout, stderr (and possibly stdin) files.
|
1060
1209
|
Possible values are:
|
@@ -1078,6 +1227,7 @@ async def create_slurmprocess_submit(jobname: str,
|
|
1078
1227
|
"""
|
1079
1228
|
proc = SlurmProcess(jobname=jobname, sbatch_script=sbatch_script,
|
1080
1229
|
workdir=workdir, time=time,
|
1230
|
+
sbatch_options=sbatch_options,
|
1081
1231
|
stdfiles_removal=stdfiles_removal,
|
1082
1232
|
**kwargs)
|
1083
1233
|
await proc.submit(stdin=stdin)
|
@@ -305,6 +305,7 @@ class SlurmTrajectoryFunctionWrapper(TrajectoryFunctionWrapper):
|
|
305
305
|
"""
|
306
306
|
|
307
307
|
def __init__(self, executable, sbatch_script,
|
308
|
+
sbatch_options: dict | None = None,
|
308
309
|
call_kwargs: typing.Optional[dict] = None,
|
309
310
|
load_results_func=None, **kwargs):
|
310
311
|
"""
|
@@ -325,6 +326,19 @@ class SlurmTrajectoryFunctionWrapper(TrajectoryFunctionWrapper):
|
|
325
326
|
|
326
327
|
- {cmd_str} : Replaced by the command to call the executable on a given trajectory.
|
327
328
|
|
329
|
+
sbatch_options : dict or None
|
330
|
+
Dictionary of sbatch options, keys are long names for options,
|
331
|
+
values are the correponding values. The keys/long names are given
|
332
|
+
without the dashes, e.g. to specify "--mem=1024" the dictionary
|
333
|
+
needs to be {"mem": "1024"}. To specify options without values use
|
334
|
+
keys with empty strings as values, e.g. to specify "--contiguous"
|
335
|
+
the dictionary needs to be {"contiguous": ""}.
|
336
|
+
See the SLURM documentation for a full list of sbatch options
|
337
|
+
(https://slurm.schedmd.com/sbatch.html).
|
338
|
+
Note: This argument is passed as is to the `SlurmProcess` in which
|
339
|
+
the computation is performed. Each call of the TrajectoryFunction
|
340
|
+
triggers the creation of a new `SlurmProcess` and will use the then
|
341
|
+
current `sbatch_options`.
|
328
342
|
call_kwargs : dict, optional
|
329
343
|
Dictionary of additional arguments to pass to the executable, they
|
330
344
|
will be added to the call as pair ' {key} {val}', note that in case
|
@@ -352,6 +366,7 @@ class SlurmTrajectoryFunctionWrapper(TrajectoryFunctionWrapper):
|
|
352
366
|
sbatch_script = f.read()
|
353
367
|
# (possibly) use properties to calc the id directly
|
354
368
|
self.sbatch_script = sbatch_script
|
369
|
+
self.sbatch_options = sbatch_options
|
355
370
|
self.executable = executable
|
356
371
|
if call_kwargs is None:
|
357
372
|
call_kwargs = {}
|
@@ -529,13 +544,14 @@ class SlurmTrajectoryFunctionWrapper(TrajectoryFunctionWrapper):
|
|
529
544
|
await _SEMAPHORES["SLURM_MAX_JOB"].acquire()
|
530
545
|
try: # this try is just to make sure we always release the semaphore
|
531
546
|
slurm_proc = await slurm.create_slurmprocess_submit(
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
547
|
+
jobname=self.slurm_jobname,
|
548
|
+
sbatch_script=sbatch_fname,
|
549
|
+
workdir=slurm_workdir,
|
550
|
+
sbatch_options=self.sbatch_options,
|
551
|
+
stdfiles_removal="success",
|
552
|
+
stdin=None,
|
553
|
+
# sleep 5 s between checking
|
554
|
+
sleep_time=5,
|
539
555
|
)
|
540
556
|
# wait for the slurm job to finish
|
541
557
|
# also cancel the job when this future is canceled
|
asyncmd/trajectory/propagate.py
CHANGED
@@ -599,14 +599,14 @@ class ConditionalTrajectoryPropagator(_TrajectoryPropagator):
|
|
599
599
|
# sort out if we use max_frames or max_steps
|
600
600
|
if max_frames is not None and max_steps is not None:
|
601
601
|
logger.warning("Both max_steps and max_frames given. Note that "
|
602
|
-
|
602
|
+
"max_steps will take precedence.")
|
603
603
|
if max_steps is not None:
|
604
604
|
self.max_steps = max_steps
|
605
605
|
elif max_frames is not None:
|
606
606
|
self.max_steps = max_frames * nstout
|
607
607
|
else:
|
608
608
|
logger.info("Neither max_frames nor max_steps given. "
|
609
|
-
|
609
|
+
"Setting max_steps to infinity.")
|
610
610
|
# this is a float but can be compared to ints
|
611
611
|
self.max_steps = np.inf
|
612
612
|
|
@@ -629,10 +629,10 @@ class ConditionalTrajectoryPropagator(_TrajectoryPropagator):
|
|
629
629
|
# and warn if it is not a corotinefunction
|
630
630
|
logger.warning(
|
631
631
|
"It is recommended to use coroutinefunctions for all "
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
632
|
+
"conditions. This can easily be achieved by wrapping any "
|
633
|
+
"function in a TrajectoryFunctionWrapper. All "
|
634
|
+
"non-coroutine condition functions will be blocking when "
|
635
|
+
"applied! ([c is coroutine for c in conditions] = %s)",
|
636
636
|
self._condition_func_is_coroutine
|
637
637
|
)
|
638
638
|
self._conditions = conditions
|
@@ -744,9 +744,10 @@ class ConditionalTrajectoryPropagator(_TrajectoryPropagator):
|
|
744
744
|
# gets the frame with the lowest idx where any condition is True
|
745
745
|
min_idx = np.argmin(frame_nums)
|
746
746
|
first_condition_fullfilled = conds_fullfilled[min_idx]
|
747
|
-
logger.error(
|
748
|
-
|
749
|
-
|
747
|
+
logger.error("Starting configuration (%s) is already fullfilling "
|
748
|
+
"the condition with idx %s.",
|
749
|
+
starting_configuration, first_condition_fullfilled,
|
750
|
+
)
|
750
751
|
# we just return the starting configuration/trajectory
|
751
752
|
trajs = [starting_configuration]
|
752
753
|
return trajs, first_condition_fullfilled
|
asyncmd/trajectory/trajectory.py
CHANGED
@@ -820,9 +820,9 @@ class Trajectory:
|
|
820
820
|
# default cache is set to h5py (as above)
|
821
821
|
logger.warning("Trying to unpickle %s with cache_type "
|
822
822
|
"'h5py' not possible without a registered "
|
823
|
-
"cache. Falling back to global default type."
|
824
|
-
"See 'asyncmd.config.register_h5py_cache' and"
|
825
|
-
"
|
823
|
+
"cache. Falling back to global default type. "
|
824
|
+
"See 'asyncmd.config.register_h5py_cache' and "
|
825
|
+
"'asyncmd.config.set_default_cache_type'.",
|
826
826
|
self
|
827
827
|
)
|
828
828
|
self.cache_type = None # this calls _setup_cache
|
@@ -949,9 +949,9 @@ class TrajectoryFunctionValueCacheNPZ(collections.abc.Mapping):
|
|
949
949
|
# now if the old npz did not match we should remove it
|
950
950
|
# then we will rewrite it with the first cached CV values
|
951
951
|
if not existing_npz_matches:
|
952
|
-
logger.debug("Found existing npz file (%s) but the"
|
953
|
-
"
|
954
|
-
"
|
952
|
+
logger.debug("Found existing npz file (%s) but the "
|
953
|
+
"trajectory hash does not match. "
|
954
|
+
"Recreating the npz cache from scratch.",
|
955
955
|
self.fname_npz
|
956
956
|
)
|
957
957
|
os.unlink(self.fname_npz)
|
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: asyncmd
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.3
|
4
4
|
Summary: asyncmd is a library to write concurrent code to run and analyze molecular dynamics simulations using pythons async/await synthax.
|
5
5
|
Author-email: Hendrik Jung <hendrik.jung@biophys.mpg.de>
|
6
6
|
Maintainer-email: Hendrik Jung <hendrik.jung@biophys.mpg.de>
|
@@ -24,6 +24,7 @@ License-File: LICENSE
|
|
24
24
|
Requires-Dist: aiofiles
|
25
25
|
Requires-Dist: mdanalysis
|
26
26
|
Requires-Dist: numpy
|
27
|
+
Requires-Dist: scipy
|
27
28
|
Provides-Extra: docs
|
28
29
|
Requires-Dist: sphinx; extra == "docs"
|
29
30
|
Provides-Extra: tests
|
@@ -36,6 +37,7 @@ Requires-Dist: coverage; extra == "tests-all"
|
|
36
37
|
Requires-Dist: pytest-cov; extra == "tests-all"
|
37
38
|
Provides-Extra: dev
|
38
39
|
Requires-Dist: asyncmd[docs,tests-all]; extra == "dev"
|
40
|
+
Dynamic: license-file
|
39
41
|
|
40
42
|
# asyncmd
|
41
43
|
|
@@ -50,7 +52,7 @@ This library addresses the tedious, error-prone and boring part of setting up ma
|
|
50
52
|
|
51
53
|
## Code Example
|
52
54
|
|
53
|
-
Run `N`
|
55
|
+
Run `N` [GROMACS] engines concurently from configurations randomly picked up along a trajectory (`traj.trr`) for `n_steps` integration steps each, drawing random Maxwell-Boltzmann velocities for each configuration on the way. Finally turn the python function `func` (which acts on `Trajectory` objects) into an asyncronous and cached function by wrapping it and apply it on all generated trajectories concurrently:
|
54
56
|
|
55
57
|
```python
|
56
58
|
import asyncio
|
@@ -103,6 +105,14 @@ For an in-depth introduction see also the `examples` folder in this repository w
|
|
103
105
|
|
104
106
|
## Installation
|
105
107
|
|
108
|
+
### pip install from PyPi
|
109
|
+
|
110
|
+
asyncmd is published on [PyPi] (since v0.3.2), installing is as easy as:
|
111
|
+
|
112
|
+
```bash
|
113
|
+
pip install asyncmd
|
114
|
+
```
|
115
|
+
|
106
116
|
### pip install directly from the repository
|
107
117
|
|
108
118
|
Please note that you need to have [git-lfs] (an open source git extension) setup to get all input files needed to run the notebooks in the `examples` folder. However, no [git-lfs] is needed to get a working version of the library.
|
@@ -113,7 +123,7 @@ cd asyncmd
|
|
113
123
|
pip install .
|
114
124
|
```
|
115
125
|
|
116
|
-
## API Reference
|
126
|
+
## Documentation and API Reference
|
117
127
|
|
118
128
|
The documentation can be build with [sphinx], use e.g. the following to build it in html format:
|
119
129
|
|
@@ -132,6 +142,9 @@ Tests use [pytest]. To run them just install asycmd with the test requirements
|
|
132
142
|
git clone https://github.com/bio-phys/asyncmd.git
|
133
143
|
cd asyncmd
|
134
144
|
pip install .\[tests\]
|
145
|
+
# or use
|
146
|
+
pip install .\[tests-all\]
|
147
|
+
# to also install optional dependencies needed to run all tests
|
135
148
|
```
|
136
149
|
|
137
150
|
And then run the tests (against the installed version) as
|
@@ -173,6 +186,8 @@ asyncmd is under the terms of the GNU general public license version 3 or later,
|
|
173
186
|
|
174
187
|
[coverage]: https://pypi.org/project/coverage/
|
175
188
|
[git-lfs]: https://git-lfs.com/
|
189
|
+
[GROMACS]: https://www.gromacs.org/
|
190
|
+
[PyPi]: https://pypi.org/project/asyncmd/
|
176
191
|
[pytest]: https://docs.pytest.org/en/latest/
|
177
192
|
[pytest-cov]: https://pypi.org/project/pytest-cov/
|
178
193
|
[SLURM]: https://slurm.schedmd.com/documentation.html
|
@@ -0,0 +1,23 @@
|
|
1
|
+
asyncmd/__init__.py,sha256=0xkOmcT5JP7emeGl9ixPSPsdHylHtiWqvZGnzDC2GqM,768
|
2
|
+
asyncmd/_config.py,sha256=PpPYgsZyPmC5uybMc0pdAMj_6jUcm8AfME1hAvYy3DE,1140
|
3
|
+
asyncmd/_version.py,sha256=14Lpt2lrPMrGonuCi12MdigFAFaEhXYlip7X18BJQwc,2981
|
4
|
+
asyncmd/config.py,sha256=Jf_XTvXPkWdefuB9xVfCUzW4Fe3zGaUB-ZifC-oZ-yQ,7701
|
5
|
+
asyncmd/mdconfig.py,sha256=rV1eaYakFnxP8aG4_86M1uv3vtFqvLwpd7YbN3bSSQc,17482
|
6
|
+
asyncmd/mdengine.py,sha256=PPWphMBSkIoinbUpLHxbEn1yI02VHrCHGKzJlSOhYq4,4127
|
7
|
+
asyncmd/slurm.py,sha256=fHXb3B4MR8c6J97T-X_t6p-4BnWG4kANfGu8HqhmPCA,60851
|
8
|
+
asyncmd/tools.py,sha256=JS4Cn2Elm6miJmPf88ZDTaUIIFqb9bhHLlZKmJOOpC0,2554
|
9
|
+
asyncmd/utils.py,sha256=cVWgZlAa3I4eE3EacIfz82HIBznldqoxyUZ7M36hMXs,5331
|
10
|
+
asyncmd/gromacs/__init__.py,sha256=Rp_FQq_ftynX1561oD1X2Rby3Y4EBtXkgNgqa4jQk2o,726
|
11
|
+
asyncmd/gromacs/mdconfig.py,sha256=6odhq8-xw6DRwQmiZIWCt5TwheyiC_7v5iUinG2T7J8,18225
|
12
|
+
asyncmd/gromacs/mdengine.py,sha256=rV5-ugbaWtmDXiNHEq8dWlpBybMwE8zWESeikal2lHU,54276
|
13
|
+
asyncmd/gromacs/utils.py,sha256=nmpP1ZS9sYPnrJGTVam6uDBb3CjKn-s0Nn3666ynnCo,6718
|
14
|
+
asyncmd/trajectory/__init__.py,sha256=m2mP6YxMpXTpU_NQ-wID4UVEgZd7e0mkSaCCLLeo75Q,1195
|
15
|
+
asyncmd/trajectory/convert.py,sha256=3DYpt7w_AIryShT59dTPM1s9s7ND3AMqgF5z8pfUViM,26036
|
16
|
+
asyncmd/trajectory/functionwrapper.py,sha256=_atGGe2Dwu_EkyC6eNkONbc0KCDLegw0GqHfoc5MGLs,25788
|
17
|
+
asyncmd/trajectory/propagate.py,sha256=LvO3vj9JHv5EiZkuNB9PTLpHuS6qy3nzZTxge5byW20,45344
|
18
|
+
asyncmd/trajectory/trajectory.py,sha256=TouClq90m__q2qkBNNCZP2p59zWeqDa5TykdUpk7WPE,50700
|
19
|
+
asyncmd-0.3.3.dist-info/licenses/LICENSE,sha256=tqi_Y64slbCqJW7ndGgNe9GPIfRX2nVGb3YQs7FqzE4,34670
|
20
|
+
asyncmd-0.3.3.dist-info/METADATA,sha256=tQ0wPLn1GTRMOysb9cUQznSHeWCMQywbGB2aKerJmAo,8389
|
21
|
+
asyncmd-0.3.3.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
|
22
|
+
asyncmd-0.3.3.dist-info/top_level.txt,sha256=YG6cpLOyBjjelv7a8p2xYEHNVBgXSW8PvM8-9S9hMb8,8
|
23
|
+
asyncmd-0.3.3.dist-info/RECORD,,
|
asyncmd-0.3.2.dist-info/RECORD
DELETED
@@ -1,23 +0,0 @@
|
|
1
|
-
asyncmd/__init__.py,sha256=0xkOmcT5JP7emeGl9ixPSPsdHylHtiWqvZGnzDC2GqM,768
|
2
|
-
asyncmd/_config.py,sha256=PpPYgsZyPmC5uybMc0pdAMj_6jUcm8AfME1hAvYy3DE,1140
|
3
|
-
asyncmd/_version.py,sha256=14Lpt2lrPMrGonuCi12MdigFAFaEhXYlip7X18BJQwc,2981
|
4
|
-
asyncmd/config.py,sha256=Jf_XTvXPkWdefuB9xVfCUzW4Fe3zGaUB-ZifC-oZ-yQ,7701
|
5
|
-
asyncmd/mdconfig.py,sha256=07TL4EJs-d_34PBSINUas3creopJX8VHfcgmuW-Wd-E,17485
|
6
|
-
asyncmd/mdengine.py,sha256=PPWphMBSkIoinbUpLHxbEn1yI02VHrCHGKzJlSOhYq4,4127
|
7
|
-
asyncmd/slurm.py,sha256=IgnWv6Jhu-PSdHTy0GB3AIlwkFcHZ12bBZy2VggSmKs,54642
|
8
|
-
asyncmd/tools.py,sha256=JS4Cn2Elm6miJmPf88ZDTaUIIFqb9bhHLlZKmJOOpC0,2554
|
9
|
-
asyncmd/utils.py,sha256=cVWgZlAa3I4eE3EacIfz82HIBznldqoxyUZ7M36hMXs,5331
|
10
|
-
asyncmd/gromacs/__init__.py,sha256=Rp_FQq_ftynX1561oD1X2Rby3Y4EBtXkgNgqa4jQk2o,726
|
11
|
-
asyncmd/gromacs/mdconfig.py,sha256=rxhp5W3Cc0KuLN_P43ItlXI_xnAiJeh3DLeTjHp57kc,18224
|
12
|
-
asyncmd/gromacs/mdengine.py,sha256=JZA3c0i2I1SK2iivNavqg_i3QFdtOOuUHbcKNlKJFho,53164
|
13
|
-
asyncmd/gromacs/utils.py,sha256=TCQ3OAtUNN7cVGjJGkEdmhZF2QvnnmazvklbQrW0AB8,6716
|
14
|
-
asyncmd/trajectory/__init__.py,sha256=m2mP6YxMpXTpU_NQ-wID4UVEgZd7e0mkSaCCLLeo75Q,1195
|
15
|
-
asyncmd/trajectory/convert.py,sha256=3DYpt7w_AIryShT59dTPM1s9s7ND3AMqgF5z8pfUViM,26036
|
16
|
-
asyncmd/trajectory/functionwrapper.py,sha256=6Oeoqi8IiffNO6egh1aRX6QSy4RyIPEYi3j64pbg-4I,24785
|
17
|
-
asyncmd/trajectory/propagate.py,sha256=TSpMw4dYodRowltdPWf5styZtUBkQJ5HlSKaV38yUuw,45335
|
18
|
-
asyncmd/trajectory/trajectory.py,sha256=HSPwEIdRZuxteRmxEi8lC4ZjCMxbPVy7QjpGBkSAKBI,50699
|
19
|
-
asyncmd-0.3.2.dist-info/LICENSE,sha256=tqi_Y64slbCqJW7ndGgNe9GPIfRX2nVGb3YQs7FqzE4,34670
|
20
|
-
asyncmd-0.3.2.dist-info/METADATA,sha256=4SlYWFxOj9lsGDF12HtYwgt8-kr2fJ9aKdwnDwX79zQ,8014
|
21
|
-
asyncmd-0.3.2.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
22
|
-
asyncmd-0.3.2.dist-info/top_level.txt,sha256=YG6cpLOyBjjelv7a8p2xYEHNVBgXSW8PvM8-9S9hMb8,8
|
23
|
-
asyncmd-0.3.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|