qdir 1.0.0__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.
qdir/__init__.py ADDED
@@ -0,0 +1,288 @@
1
+ #!/bin/env python3
2
+ """A very low overhead first-in, first-out job queue for the local machine
3
+
4
+ The queue is maintained as a directory of tiny job status files.
5
+ When a new job is submitted, it will identify any existing status files and
6
+ then add its own to the directory.
7
+ The job will not launch its command until all the preceeding status files are gone.
8
+ Each job will delete its own status file upon completion so the next job can begin.
9
+ There is a polling interval (default 5 seconds)
10
+ when the job checks to see if its predecessors have finished before launching its command.
11
+ A shared queue directory can be specified on a multi-user system to coordinate
12
+ job execution.
13
+ On Linux this is `/tmp` by default.
14
+ """
15
+
16
+ __author__ = "Carl Osterwisch"
17
+ __version__ = "1.0.0"
18
+ __url__ = "https://github.com/costerwi/qdir/"
19
+
20
+ import os
21
+ import signal
22
+ import subprocess
23
+ import sys
24
+ from argparse import ArgumentParser, Namespace
25
+ from pathlib import Path
26
+ from tempfile import gettempdir
27
+ from time import asctime, perf_counter, sleep
28
+
29
+ from filelock import FileLock
30
+
31
+ default = Namespace
32
+
33
+ parser = ArgumentParser(epilog=f"qdir {__version__} {__url__}")
34
+ default.queue = Path(os.environ.get("QDIR_QUEUE", gettempdir()))
35
+ parser.add_argument(
36
+ "-q",
37
+ "--queue",
38
+ type=Path,
39
+ help="Directory to store the queue files. "
40
+ f"Default is environment variable QDIR_QUEUE or {default.queue}",
41
+ default=default.queue,
42
+ )
43
+ default.verbose = int(os.environ.get("QDIR_VERBOSE", "2"))
44
+ parser.add_argument(
45
+ "-v",
46
+ "--verbose",
47
+ type=int,
48
+ help="Verbosity level. "
49
+ f"Default is environment variable QDIR_VERBOSE or {default.verbose}",
50
+ default=default.verbose,
51
+ )
52
+
53
+ qFileSuffix = os.environ.get("QDIR_SUFFIX", ".qdir") # suffix for queue files
54
+
55
+
56
+ class Queue:
57
+ def __init__(self, **kwargs):
58
+ """Create the queue, allowing parameters to override defaults"""
59
+ for parameter, value in vars(default).items():
60
+ setattr(self, parameter, kwargs.pop(parameter, value))
61
+ if not self.queue.is_dir():
62
+ raise FileNotFoundError(f"The QDIR directory {self.queue} does not exist")
63
+ if not os.access(self.queue, os.W_OK):
64
+ raise PermissionError(f"The QDIR directory {self.queue} is not writable")
65
+
66
+ def getQueuedJobs(self) -> list[Path]:
67
+ """Return a list of qFilePath for currently queued jobs, sorted oldest to newest."""
68
+
69
+ queuedJobs = [filePath for filePath in self.queue.glob("*" + qFileSuffix)]
70
+ if len(queuedJobs) == 0:
71
+ return queuedJobs # easy answer
72
+
73
+ # Collecct a set of active process ids
74
+ if os.name == "nt":
75
+ import win32process
76
+
77
+ processIds = {str(pid) for pid in win32process.EnumProcesses()}
78
+ else:
79
+ procPath = Path("/proc")
80
+ if procPath.is_dir():
81
+ processIds = {p.stem for p in procPath.iterdir() if p.stem.isnumeric()}
82
+ else:
83
+ c = subprocess.run(
84
+ ["pgrep", "y"], stdout=subprocess.PIPE, text=True, check=True
85
+ )
86
+ processIds = {pid for pid in c.stdout.strip().split()}
87
+
88
+ def is_qFile(path):
89
+ """Verify the path is an active job file with a matching process id"""
90
+ if not path.stem.isnumeric():
91
+ return False # file name must be an integer
92
+ if not (path.is_file()):
93
+ return False # path must represent a file
94
+ if path.stem in processIds:
95
+ return True # file name must match a running process number
96
+
97
+ # Otherwise, the process may have died or the computer was rebooted with
98
+ # this job in the queue. This orphaned qFile may be removed.
99
+ print(
100
+ "Removing orphaned queue file",
101
+ self.info.format(**self.getJobInfo(path)),
102
+ file=sys.stderr,
103
+ )
104
+ try:
105
+ path.unlink(missing_ok=True)
106
+ except OSError as e:
107
+ print(e, file=sys.stderr)
108
+
109
+ # Filter valid qFiles
110
+ queuedJobs = [filePath for filePath in queuedJobs if is_qFile(filePath)]
111
+ return sorted(queuedJobs, key=lambda qFilePath: qFilePath.stat().st_mtime)
112
+
113
+ def getJobInfo(self, qFilePath: Path) -> dict:
114
+ """Return information about the job specified in qFilePath"""
115
+ jobInfo = {
116
+ "jobId": qFilePath.stem,
117
+ "owner": qFilePath.owner(),
118
+ "name": "",
119
+ "command": "",
120
+ "directory": "",
121
+ "behind": "",
122
+ "status": "",
123
+ "history": [], # list[str]
124
+ }
125
+ try:
126
+ with qFilePath.open() as qFile:
127
+ for line in qFile:
128
+ keyValue = line.strip().split("\t", 2)
129
+ if len(keyValue) == 2:
130
+ jobInfo.update([keyValue])
131
+ else:
132
+ status = keyValue[0]
133
+ jobInfo["history"].append(status)
134
+ jobInfo["status"] = status # latest update
135
+ except OSError as e:
136
+ status = repr(e)
137
+ jobInfo["history"].append(status)
138
+ jobInfo["status"] = status
139
+ return jobInfo
140
+
141
+ def dequeue(self, qFilePath: Path):
142
+ """Remove a job from the queue"""
143
+ assert qFilePath.parent == self.queue
144
+ assert qFilePath.suffix == qFileSuffix
145
+ assert qFilePath.stem.isnumeric()
146
+ # Removing qFile will dequeue the job but the effect is delayed by polling interval
147
+ qFilePath.unlink(missing_ok=True)
148
+ if os.name == "posix":
149
+ # On Linux the SIGTERM signal has immediate effect
150
+ try:
151
+ os.kill(int(qFilePath.stem), signal.SIGTERM)
152
+ except OSError: # no such process
153
+ pass
154
+
155
+ def enqueue(self, cmd: list[str]):
156
+ """Wait in queue until prior jobs finish, then run cmd"""
157
+ import getpass
158
+ import shutil
159
+
160
+ if len(cmd) < 1:
161
+ raise SyntaxError("Missing cmd")
162
+ if self.verbose >= 1 and not shutil.which(cmd[0]):
163
+ print(f"WARNING: {cmd[0]} is not in PATH", file=sys.stderr)
164
+ if not self.interval > 0:
165
+ self.interval = 1
166
+
167
+ startTime = perf_counter() # time job was enqueued
168
+ job = None # subprocess running the cmd, if any
169
+
170
+ jobInfo = { # information about this job
171
+ "jobId": str(os.getpid()), # process id of this script
172
+ "owner": getpass.getuser(),
173
+ "name": self.name or cmd[-1],
174
+ "command": " ".join(cmd),
175
+ "directory": Path.cwd(),
176
+ }
177
+
178
+ def updateStatus(priority, status, file=None):
179
+ """Notify user of status change and optionally record in file"""
180
+ jobInfo["status"] = status
181
+ jobInfo["time"] = asctime()
182
+ jobInfo["duration"] = humanDuration(perf_counter() - startTime)
183
+ if self.verbose >= priority:
184
+ # Echo status to the terminal
185
+ print(self.status.format(**jobInfo))
186
+ if file is not None:
187
+ # Record status history in specified qFile
188
+ print("{time} {status}".format(**jobInfo), file=file)
189
+
190
+ qFilePath = (self.queue / jobInfo["jobId"]).with_suffix(qFileSuffix)
191
+ lock = FileLock(self.queue / "qdir.lock") # prevent previousJobs race
192
+ with lock.acquire(timeout=5):
193
+ previousJobs = self.getQueuedJobs()
194
+ jobInfo["behind"] = " ".join([job.stem for job in previousJobs])
195
+ with qFilePath.open("w") as qFile:
196
+ os.environ["QDIR_FILE"] = qFile.name # will be exported to cmd
197
+ # Initialize qFile contents
198
+ for key in "name", "command", "directory", "behind":
199
+ value = jobInfo.get(key)
200
+ if value:
201
+ print(key, value, sep="\t", file=qFile)
202
+ updateStatus(priority=2, status="Queued", file=qFile)
203
+
204
+ def terminateJob(sig=None, frame=None):
205
+ """Terminate this job if running or dequeue if not"""
206
+ nonlocal job
207
+ result = ""
208
+ qFilePath.unlink(missing_ok=True) # remove its qFile
209
+ if job is None:
210
+ # removed from the queue before starting
211
+ result = ".. Dequeued"
212
+ elif hasattr(job, "pid"): # job is or was running
213
+ if job.poll() is None:
214
+ # job is still running
215
+ if self.verbose >= 2:
216
+ result = ".. Killed"
217
+ if os.name == "nt":
218
+ job.send_signal(signal.CTRL_BREAK_EVENT)
219
+ else:
220
+ # pass SIGTERM signal to all child processes in this process group
221
+ os.killpg(os.getpgid(job.pid), signal.SIGTERM)
222
+ elif self.verbose >= 1:
223
+ # job is no longer running; check returncode
224
+ if job.returncode <= 0:
225
+ result = "** Done"
226
+ else:
227
+ result = "-- Error"
228
+ if result:
229
+ updateStatus(priority=2, status=result)
230
+ job = result # prevent this from duplicate reporting
231
+
232
+ signal.signal(
233
+ signal.SIGTERM, terminateJob
234
+ ) # catch termination signal (ignored on Windows)
235
+
236
+ # Wait for all previousJobs to complete
237
+ previousJobs.reverse() # check newest jobs first
238
+ while qFilePath.exists() and any(
239
+ otherFile.exists() for otherFile in previousJobs
240
+ ):
241
+ sleep(self.interval)
242
+
243
+ # Launch the cmd only if this job's qFile still exists
244
+ if qFilePath.exists():
245
+ try:
246
+ # Append the job starting status to qFile
247
+ stat = qFilePath.stat()
248
+ with qFilePath.open("a") as qFile:
249
+ print(
250
+ "Waited in queue",
251
+ humanDuration(perf_counter() - startTime),
252
+ file=qFile,
253
+ )
254
+ updateStatus(priority=3, status="Started", file=qFile)
255
+ # Keep mtime unchanged so the job files sort correctly
256
+ os.utime(qFilePath, (stat.st_atime, stat.st_mtime))
257
+ except OSError:
258
+ pass # ignore status update exceptions
259
+ logFilePath = Path(self.output.format(**jobInfo))
260
+ with logFilePath.open("w") as logFile:
261
+ opts = {}
262
+ if os.name == "nt":
263
+ opts["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
264
+ try:
265
+ job = subprocess.Popen(
266
+ cmd,
267
+ shell=False,
268
+ start_new_session=True,
269
+ stdin=subprocess.DEVNULL,
270
+ stdout=logFile,
271
+ stderr=subprocess.STDOUT,
272
+ **opts,
273
+ )
274
+ startTime = perf_counter() # reset to time job started running
275
+ while qFilePath.exists() and job.poll() is None:
276
+ # job is still running
277
+ sleep(self.interval)
278
+ except OSError as e:
279
+ print(e, file=logFile)
280
+
281
+ terminateJob() # Clean up at the end
282
+
283
+
284
+ def humanDuration(seconds: float) -> str:
285
+ for unit, per in ("days", 86400), ("hours", 3600), ("minutes", 60):
286
+ if seconds >= 1.1 * per:
287
+ return f"{seconds / per:.2g} {unit}"
288
+ return f"{seconds:.2g} seconds"
@@ -0,0 +1,5 @@
1
+ """Modules to help Abaqus interact with the qdir job queue
2
+
3
+ Install with:
4
+ python3 -m qdir.abaqus install
5
+ """
@@ -0,0 +1,66 @@
1
+ """Install or uninstall qdir for Abaqus.
2
+
3
+ This script will modify the user's abaqus_v6.env file with
4
+ settings for the qdir job queue.
5
+ """
6
+
7
+ from __future__ import print_function, with_statement # use Python3 syntax
8
+
9
+ import os
10
+ import sys
11
+ from argparse import ArgumentParser
12
+
13
+ settings = """# Settings for qdir job queue
14
+ sys.path.append(r"{}") # directory of qdir_submit.py
15
+ import qdir_submit
16
+ qdir_submit.init(queues)
17
+ plugin_central_dir += os.pathsep + qdir_submit.dirname
18
+ run_mode = BATCH # command line will submit to queue by default"""
19
+
20
+ parser = ArgumentParser(description=__doc__)
21
+ parser.add_argument(
22
+ "action",
23
+ choices=["install", "uninstall"],
24
+ help="Specify what to do.",
25
+ )
26
+ default = os.path.expanduser("~/abaqus_v6.env")
27
+ parser.add_argument(
28
+ "env",
29
+ nargs="?",
30
+ default=default,
31
+ help="Optionally specify the .env file to modify. Default is {}".format(default),
32
+ )
33
+
34
+
35
+ def uninstall(user_env):
36
+ """Remove qdir settings from user's abaqus_v6.env"""
37
+ new_env = user_env + ".new"
38
+ with open(new_env, "w") as new_env_file:
39
+ try:
40
+ # copy current settings
41
+ with open(user_env) as env_file:
42
+ for line in env_file:
43
+ if not "qdir" in line: # skip any qdir settings
44
+ print(line.rstrip(), file=new_env_file)
45
+ os.remove(user_env)
46
+ except FileNotFoundError:
47
+ pass
48
+ os.rename(new_env, user_env)
49
+
50
+
51
+ def install(user_env):
52
+ """Append qdir settings to user's abaqus_v6.env"""
53
+ uninstall(user_env) # remove any previous settings
54
+ dirname = os.path.dirname(os.path.normpath(__file__))
55
+ with open(user_env, "a") as user_env_file:
56
+ print(settings.format(dirname), file=user_env_file)
57
+
58
+
59
+ if __name__ == "__main__":
60
+ options = parser.parse_args(sys.argv[1:])
61
+ if "uninstall" == options.action:
62
+ print("Removing qdir settings from", options.env)
63
+ uninstall(options.env)
64
+ else:
65
+ print("Adding qdir settings to", options.env)
66
+ install(options.env)
@@ -0,0 +1,236 @@
1
+ """Abaqus CAE plugin to interact with qdir Queue"""
2
+
3
+ from __future__ import print_function, with_statement
4
+
5
+ __version__ = "1.0.0"
6
+ __author__ = "Carl Osterwisch"
7
+ __url__ = "https://github.com/costerwi/qdir/"
8
+
9
+ import json
10
+ import os
11
+ from subprocess import PIPE, Popen
12
+
13
+ from abaqusGui import *
14
+
15
+ if isinstance(u"unicode", str): # fmt: skip
16
+
17
+ def a(unicodeString):
18
+ "Do nothing for Python3, Abaqus >=2024"
19
+ return unicodeString
20
+ else:
21
+
22
+ def a(unicodeString):
23
+ "Convert unicode to str for Abaqus <2024"
24
+ return unicodeString.encode("ascii", "ignore")
25
+
26
+
27
+ class QDirQueueMonitor(AFXDataDialog):
28
+ (ID_TABLE, ID_REFRESH, ID_HELP, ID_SUBMITALL, ID_LAST) = range(
29
+ AFXDataDialog.ID_LAST, AFXDataDialog.ID_LAST + 5
30
+ )
31
+
32
+ def __init__(self, form):
33
+ AFXDataDialog.__init__(
34
+ self,
35
+ form,
36
+ title="QDir Job Queue Monitor",
37
+ opts=DIALOG_NORMAL | DECOR_RESIZE,
38
+ )
39
+ self.appendActionButton("Queue all Jobs", self, self.ID_SUBMITALL)
40
+ FXMAPFUNC(self, SEL_COMMAND, self.ID_SUBMITALL, QDirQueueMonitor.submitAll)
41
+ self.appendActionButton("Help", self, self.ID_HELP)
42
+ FXMAPFUNC(self, SEL_COMMAND, self.ID_HELP, QDirQueueMonitor.getHelp)
43
+ self.appendActionButton(self.DISMISS)
44
+ self.updateTimer = None
45
+ self.headings = ("jobId", "status", "name")
46
+ self.jobData = [] # active jobs
47
+
48
+ sp = FXSplitter(self, LAYOUT_FILL_X | LAYOUT_FILL_Y | SPLITTER_VERTICAL)
49
+ self.table = AFXTable(
50
+ p=sp,
51
+ numVisRows=6,
52
+ numVisColumns=1 + len(self.headings),
53
+ numRows=1,
54
+ numColumns=1 + len(self.headings),
55
+ tgt=self,
56
+ sel=self.ID_TABLE,
57
+ opts=AFXTABLE_NORMAL | AFXTABLE_ROW_MODE,
58
+ )
59
+ FXMAPFUNC(self, SEL_SELECTED, self.ID_TABLE, QDirQueueMonitor.onSelected)
60
+ FXMAPFUNC(self, SEL_DELETED, self.ID_TABLE, QDirQueueMonitor.onDelete)
61
+ self.table.setLeadingRows(numRows=1)
62
+ self.table.setLeadingColumns(numColumns=1)
63
+ self.table.setLeadingRowLabels("\t".join([h.title() for h in self.headings]))
64
+ try:
65
+ c = self.headings.index("jobId")
66
+ self.table.setColumnWidth(column=1 + c, width=0) # Hide the jobIds
67
+ except ValueError:
68
+ pass
69
+ try:
70
+ c = self.headings.index("status")
71
+ self.table.setColumnWidthInChars(column=1 + c, numChars=30)
72
+ except ValueError:
73
+ pass
74
+ self.table.setStretchableColumn(
75
+ len(self.headings)
76
+ ) # Last column expands with dialog
77
+ self.table.setPopupOptions(AFXTable.POPUP_DELETE_ROW)
78
+ self.jobDetails = FXText(p=sp, opts=TEXT_READONLY | LAYOUT_FILL_Y)
79
+ self.jobDetails.setVisRows(5)
80
+ try:
81
+ self.interval = max(1, int(os.environ.get("QDIR_POLL_INTERVAL", "5")))
82
+ except ValueError:
83
+ self.interval = 5
84
+ FXMAPFUNC(self, SEL_TIMEOUT, self.ID_REFRESH, QDirQueueMonitor.updateTable)
85
+
86
+ def qstat(self):
87
+ """Collect jobInfo of currently queued jobs"""
88
+ self.jobData = []
89
+ cmd = ["python3", "-m", "qdir.qstat", "--json"]
90
+ env = os.environ.copy()
91
+ env["PYTHONPATH"] = "" # remove Abaqus modified PYTHONPATH
92
+ try:
93
+ process = Popen(cmd, stdout=PIPE, stderr=PIPE, env=env)
94
+ stdout, stderr = process.communicate()
95
+ returncode = process.returncode
96
+ except OSError as e:
97
+ returncode = "OSError"
98
+ stdout = ""
99
+ stderr = str(e)
100
+ if returncode:
101
+ # report error data
102
+ jobInfo = {
103
+ "status": "Error {}".format(returncode),
104
+ "name": " ".join(cmd),
105
+ "stdout": stdout,
106
+ "stderr": stderr,
107
+ }
108
+ self.jobData.append(jobInfo)
109
+ else:
110
+ self.jobData.extend(json.loads(stdout))
111
+
112
+ def detailedText(self, jobInfo):
113
+ """Summarize full job status"""
114
+ h = []
115
+ for key, value in jobInfo.items():
116
+ if key in ("status", "position"):
117
+ continue
118
+ if not value:
119
+ continue
120
+ if isinstance(value, list):
121
+ h.append(key.upper())
122
+ h.extend(value)
123
+ else:
124
+ h.append("{}\t{}".format(key.upper(), value))
125
+ return "\n".join(h)
126
+
127
+ def updateTable(self, *args):
128
+ """Update manager with list of currently queued jobs"""
129
+ row = self.table.getCurrentRow()
130
+ selected = None
131
+ if row > 0:
132
+ try:
133
+ c = self.headings.index("jobId")
134
+ selected = self.table.getItemText(row, column=1 + c)
135
+ except ValueError:
136
+ pass
137
+ self.qstat()
138
+ diff = len(self.jobData) + 1 - self.table.getNumRows()
139
+ if diff < 0:
140
+ self.table.deleteRows(
141
+ startRow=1,
142
+ numRows=-diff,
143
+ notify=FALSE, # delete from top
144
+ )
145
+ elif diff > 0:
146
+ self.table.insertRows(
147
+ startRow=self.table.getNumRows(), # add to bottom
148
+ numRows=diff,
149
+ notify=FALSE,
150
+ )
151
+ jobDetails = ""
152
+ for row, jobInfo in enumerate(self.jobData, 1):
153
+ for column, heading in enumerate(self.headings, 1):
154
+ self.table.setItemValue(
155
+ row=row,
156
+ column=column,
157
+ valueText=a(jobInfo.get(heading, "")),
158
+ )
159
+ if jobInfo["jobId"] == selected or (selected is None and row == 1):
160
+ jobDetails = self.detailedText(jobInfo)
161
+ self.jobDetails.setText(a(jobDetails))
162
+ self.updateTimer = getAFXApp().addTimeout(
163
+ 1000 * self.interval, self, self.ID_REFRESH
164
+ )
165
+
166
+ def onSelected(self, sender, sel, ptr):
167
+ """Show the details for selected job"""
168
+ row = sender.getCurrentRow()
169
+ if row > 0:
170
+ jobInfo = self.jobData[row - 1]
171
+ jobDetails = self.detailedText(jobInfo)
172
+ self.jobDetails.setText(a(jobDetails))
173
+
174
+ def onDelete(self, sender, sel, ptr):
175
+ """Dequeue jobs for selected table rows"""
176
+ try:
177
+ c = self.headings.index("jobId")
178
+ except ValueError:
179
+ return # missing jobId
180
+ cmd = ["python3", "-m", "qdir.qdel"]
181
+ cmd.extend(
182
+ self.table.getItemText(row, column=1 + c)
183
+ for row in range(1, self.table.getNumRows())
184
+ if self.table.isRowSelected(row)
185
+ )
186
+ if len(cmd) > 1:
187
+ self.jobDetails.setText("")
188
+ env = os.environ.copy()
189
+ env["PYTHONPATH"] = "" # remove Abaqus modified PYTHONPATH
190
+ process = Popen(cmd, env=env)
191
+ process.wait()
192
+
193
+ def submitAll(self, sender, sel, ptr):
194
+ """Submit all CAE jobs"""
195
+ sendCommand("qdir_submit.submitCAEJobs()")
196
+ return 1
197
+
198
+ def getHelp(self, sender, sel, ptr):
199
+ """Open webbrowser to plugin helpUrl"""
200
+ import webbrowser
201
+
202
+ webbrowser.open(__url__)
203
+ return 1
204
+
205
+ def show(self):
206
+ """Update the job data"""
207
+ AFXDataDialog.show(self)
208
+ self.updateTable() # update the table and start the updateTimer
209
+
210
+ def hide(self):
211
+ """Remove the updateTimer"""
212
+ if self.updateTimer is not None:
213
+ getAFXApp().removeTimeout(self.updateTimer)
214
+ self.updateTimer = None
215
+ AFXDataDialog.hide(self)
216
+
217
+
218
+ class QDirForm(AFXForm):
219
+ def getFirstDialog(self):
220
+ return QDirQueueMonitor(self)
221
+
222
+
223
+ ###########################################################################
224
+ # Register the plugin
225
+ ###########################################################################
226
+ toolset = getAFXApp().getAFXMainWindow().getPluginToolset()
227
+
228
+ toolset.registerGuiMenuButton(
229
+ buttonText="&QDir Job Queue Monitor...",
230
+ object=QDirForm(toolset),
231
+ kernelInitString="import qdir_submit # {}".format(__version__),
232
+ author=__author__,
233
+ version=str(__version__),
234
+ helpUrl=__url__,
235
+ description="Check status of jobs in the QDir Queue",
236
+ )
@@ -0,0 +1,167 @@
1
+ """Abaqus Queue class to create a job script and submit it to qdir
2
+
3
+ This module is compatible with at least Abaqus 2019 thru 2026.
4
+ This module works under Windows and Linux.
5
+
6
+ Carl Osterwisch, January 2024
7
+ """
8
+
9
+ from __future__ import print_function, with_statement # use Python3 syntax
10
+
11
+ __all__ = ["QDirQueue"]
12
+
13
+ import os
14
+ from string import Template
15
+ from textwrap import dedent
16
+
17
+ import uti
18
+ from driverQueues import Queue
19
+
20
+ dirname = os.path.dirname(os.path.normpath(__file__))
21
+
22
+
23
+ def init(queues, queue=None):
24
+ """Add the queue instance, QDirQueue() by default, to dict of solver queues"""
25
+ if queue is None:
26
+ queue = QDirQueue()
27
+ qName = getattr(queue, "name", "qdir")
28
+ queues[qName] = queue # add to solver
29
+ queues.setdefault("default", queue)
30
+ initCAE(qName) # also add to CAE
31
+ return qName
32
+
33
+
34
+ def initCAE(qName):
35
+ """Register callback to add qName to Abaqus CAE Job queues"""
36
+ try:
37
+ from abaqus import addImportCallback, session
38
+ except ImportError:
39
+ return # not running within CAE
40
+
41
+ def addQueue(*args):
42
+ """Add qName to list of selectable Abaqus CAE Job queues"""
43
+ if qName not in session.queues:
44
+ session.Queue(name=qName, queueName=qName)
45
+
46
+ addImportCallback("job", addQueue)
47
+
48
+
49
+ def submitCAEJobs(jobList=None, qName="qdir"):
50
+ """For specified CAE Jobs (all by default), set the queue and submit to queue"""
51
+ import abaqus
52
+
53
+ for job in abaqus.mdb.jobs.values():
54
+ if not jobList or job.name in jobList:
55
+ try:
56
+ print("Submitting {} to {}".format(job.name, qName))
57
+ job.setValues(queue=qName)
58
+ job.submit()
59
+ except abaqus.AbaqusException as e:
60
+ print(e)
61
+
62
+
63
+ class QDirQueue(Queue):
64
+ """Creates a job script and submits it to a qdir queue."""
65
+
66
+ def __init__(self):
67
+ if hasattr(Queue, "__init__"):
68
+ Queue.__init__(self)
69
+ self.name = "qdir"
70
+ if uti.getPlatform().startswith("win"):
71
+ # Windows OS
72
+ self.extension = ".cmd" # job script filename extension
73
+ self.setenv = "set" # shell command to set environment variables
74
+ self.template = Template( # job script template
75
+ dedent(
76
+ """\
77
+ @echo off
78
+ rem QDIR --name "$job"
79
+ rem QDIR --output "{name}.log"
80
+ set JOB="$job"
81
+ set ABAQUS="$abaqus"
82
+ $exports
83
+ cd /D "%~dp0"
84
+
85
+ set t=0
86
+ :do
87
+ if not exist %JOB%.lck goto done
88
+ set /a t+=5
89
+ echo Paused %t% seconds for %JOB%.lck to clear
90
+ ping 127.0.0.1 -n 6 >nul
91
+ if not %t%==90 goto do
92
+ :done
93
+
94
+ %ABAQUS% python -u %JOB%.com
95
+ """
96
+ )
97
+ )
98
+
99
+ else:
100
+ # Non-Windows OS
101
+ self.extension = ".sh" # job script filename extension
102
+ self.setenv = "export" # shell command to set environment variables
103
+ self.template = Template( # job script template
104
+ dedent(
105
+ """\
106
+ #!/bin/sh
107
+ #QDIR --name "$job"
108
+ #QDIR --output "{name}.log"
109
+ JOB="$job"
110
+ ABAQUS="$abaqus"
111
+ $exports
112
+ cd "$$(dirname "$$0")"
113
+
114
+ let t=0
115
+ while [[ -e "$$JOB.lck" && $$t != 90 ]]
116
+ do
117
+ let t+=5
118
+ echo Paused $$t seconds for $$JOB.lck to clear
119
+ sleep 5
120
+ done
121
+
122
+ umask 0007
123
+ exec "$$ABAQUS" python -u "$$JOB.com"
124
+ """
125
+ )
126
+ )
127
+
128
+ def exports(self, options, env):
129
+ """Return a dict of os.environ settings to be inserted into the command script"""
130
+ extraEnviron = {}
131
+ if uti.getProductVersion() == "Abaqus 2026":
132
+ extraEnviron["ABQ_DLALLOCATOR"] = "1" # example usage for BR10000415659
133
+ return extraEnviron
134
+
135
+ def createJobScript(self, options, env):
136
+ """Create shell script to run Abaqus job"""
137
+ job = options["job"]
138
+ scriptName = os.path.join(".", job + self.extension)
139
+ exports = self.exports(options, env) or ""
140
+ if isinstance(exports, dict):
141
+ # expand dict of os.environ settings to multiline str
142
+ exports = "\n".join(
143
+ self.setenv + " {0}={1}".format(*i) for i in sorted(exports.items())
144
+ )
145
+
146
+ with open(scriptName, "w") as jobFile:
147
+ print(
148
+ self.template.substitute(
149
+ abaqus=env["ABA_COMMAND"],
150
+ job=job,
151
+ exports=exports,
152
+ ),
153
+ file=jobFile,
154
+ )
155
+ try:
156
+ os.chmod(scriptName, 0o775) # Set permissions
157
+ except OSError:
158
+ pass
159
+ return scriptName
160
+
161
+ def submit(self, options, env):
162
+ """Create Abaqus job script and submit it to qdir"""
163
+ scriptName = self.createJobScript(options, env)
164
+ cmd = "python3 -m qdir.qsub {!r}".format(scriptName)
165
+ localenv = env.copy()
166
+ localenv["PYTHONPATH"] = "" # remove Abaqus modified PYTHONPATH
167
+ return self.spawn(cmd, localenv, options.get("verbose", 0))
qdir/qdel.py ADDED
@@ -0,0 +1,51 @@
1
+ #!/bin/env python3
2
+ """Dequeue (remove) specified jobs from the qdir job queue"""
3
+
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from qdir import Queue, parser
8
+
9
+ parser.description = __doc__
10
+ parser.add_argument(
11
+ "job", nargs="+", help="The list of job Ids or job names to dequeue."
12
+ )
13
+
14
+
15
+ def dequeueJobs(queue, jobList: list[str]):
16
+ """Remove a list of jobIds or job names from the queue."""
17
+ queuedJobs: dict[Path, dict] = {}
18
+ queuedJobs.update(
19
+ [
20
+ (qFilePath, queue.getJobInfo(qFilePath))
21
+ for qFilePath in queue.getQueuedJobs()
22
+ ]
23
+ )
24
+ if not queuedJobs and queue.verbose >= 2:
25
+ print("No jobs queued", file=sys.stderr)
26
+ for job in jobList:
27
+ matched = 0
28
+ for qFilePath, jobInfo in queuedJobs.items():
29
+ if job == jobInfo.get("jobId") or job == jobInfo.get("name"):
30
+ # Matched either jobId or job name
31
+ matched += 1
32
+ if queue.verbose >= 3:
33
+ print("Dequeing", queue.info.format(**jobInfo))
34
+ queue.dequeue(qFilePath)
35
+ if matched == 0 and queue.verbose >= 1:
36
+ print(job, "is not queued", file=sys.stderr)
37
+
38
+
39
+ def qdel(argv):
40
+ """Dequeue the specified jobId numbers or job names"""
41
+ options = parser.parse_args(argv[1:])
42
+ queue = Queue(**vars(options))
43
+ dequeueJobs(queue, options.job)
44
+
45
+
46
+ def main():
47
+ qdel(sys.argv)
48
+
49
+
50
+ if __name__ == "__main__":
51
+ main()
qdir/qstat.py ADDED
@@ -0,0 +1,144 @@
1
+ #!/bin/env python3
2
+ """Report currrent status of the qdir job queue"""
3
+
4
+ import json
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from qdir import Queue, default, parser
10
+
11
+ parser.description = __doc__
12
+ parser.add_argument(
13
+ "-f",
14
+ "--full",
15
+ action="store_true",
16
+ help="Report full detailed information in text format.",
17
+ )
18
+ parser.add_argument(
19
+ "-j",
20
+ "--json",
21
+ action="store_true",
22
+ help="Report full detailed queue status in json format.",
23
+ )
24
+ default.info = os.environ.get("QDIR_INFO", "{jobId:8} {owner:12} {status:35} {name}")
25
+ parser.add_argument(
26
+ "-i",
27
+ "--info",
28
+ help="Specify the shortened format of job information "
29
+ "when neither --full nor --json are specified. "
30
+ f"Default is environment variable QDIR_INFO or {default.info!r}",
31
+ default=default.info,
32
+ )
33
+ parser.add_argument(
34
+ "job",
35
+ nargs="*",
36
+ help="An optional list of job Ids or job names to get status. "
37
+ "If no jobs are specified then all currently queued jobs will be listed.",
38
+ )
39
+
40
+
41
+ if os.name == "nt":
42
+
43
+ def winOwner(self) -> str:
44
+ "Return name of the WindowsPath owner"
45
+ try:
46
+ import win32security
47
+ except ImportError:
48
+ return "unknown"
49
+ sd = win32security.GetFileSecurity(
50
+ str(self), win32security.OWNER_SECURITY_INFORMATION
51
+ )
52
+ owner_sid = sd.GetSecurityDescriptorOwner()
53
+ name, _domain, _type = win32security.LookupAccountSid(None, owner_sid)
54
+ return name
55
+
56
+ try:
57
+ _ = Path(".").owner() # test if Path has owner method
58
+ except NotImplementedError:
59
+ Path.owner = winOwner # monkey patch if needed
60
+
61
+
62
+ def printStatus(queue, jobList: list[str]):
63
+ """Print a list of queued jobs"""
64
+ if queue.verbose >= 2:
65
+ print("Queue directory", queue.queue)
66
+ queuedJobs = queue.getQueuedJobs()
67
+ if not queuedJobs and queue.verbose >= 2:
68
+ print("No jobs queued")
69
+ jobHeadings = None
70
+ for qFilePath in queuedJobs:
71
+ jobInfo = queue.getJobInfo(qFilePath)
72
+ if (
73
+ len(jobList) == 0
74
+ or jobInfo["jobId"] in jobList
75
+ or jobInfo["name"] in jobList
76
+ ):
77
+ if not jobHeadings and queue.verbose >= 1:
78
+ jobHeadings = {k: k.upper() for k in jobInfo}
79
+ print(queue.info.format(**jobHeadings))
80
+ print(queue.info.format(**jobInfo))
81
+
82
+
83
+ def printDetails(queue, jobList: list[str]):
84
+ """Report full details about queued jobs"""
85
+ if queue.verbose >= 2:
86
+ print("Queue directory", queue.queue)
87
+ queuedJobs = queue.getQueuedJobs()
88
+ if not queuedJobs and queue.verbose >= 2:
89
+ print("No jobs queued")
90
+
91
+ for position, qFilePath in enumerate(queuedJobs, 1):
92
+ jobInfo = queue.getJobInfo(qFilePath)
93
+ jobInfo["position"] = position
94
+ if (
95
+ len(jobList) == 0
96
+ or jobInfo["jobId"] in jobList
97
+ or jobInfo["name"] in jobList
98
+ ):
99
+ print()
100
+ for key, value in jobInfo.items():
101
+ if "status" == key:
102
+ continue # Will be included with full history
103
+ if isinstance(value, list):
104
+ print(key.upper(), *value, sep="\n")
105
+ elif value:
106
+ print(f"{key.upper():10} {value}")
107
+
108
+
109
+ def printJSON(queue, jobList: list[str]):
110
+ """Output detailed queue status in JSON format"""
111
+ jobs = []
112
+ for position, qFilePath in enumerate(queue.getQueuedJobs(), 1):
113
+ jobInfo = queue.getJobInfo(qFilePath)
114
+ jobInfo["position"] = position
115
+ if (
116
+ len(jobList) == 0
117
+ or jobInfo["jobId"] in jobList
118
+ or jobInfo["name"] in jobList
119
+ ):
120
+ jobs.append(jobInfo)
121
+ print(json.dumps(jobs, indent=2))
122
+
123
+
124
+ def qstat(argv):
125
+ """Report status of the queue"""
126
+ options = parser.parse_args(argv[1:])
127
+ queue = Queue(**vars(options))
128
+ if options.full:
129
+ # Report detailed information about jobs in the queue
130
+ printDetails(queue, options.job)
131
+ elif options.json:
132
+ # Report detailed queue status in json format
133
+ printJSON(queue, options.job)
134
+ else:
135
+ # Report list of jobs in queue
136
+ printStatus(queue, options.job)
137
+
138
+
139
+ def main():
140
+ qstat(sys.argv)
141
+
142
+
143
+ if __name__ == "__main__":
144
+ main()
qdir/qsub.py ADDED
@@ -0,0 +1,139 @@
1
+ #!/bin/env python3
2
+ """Submit a job (command) to the qdir job queue"""
3
+
4
+ import os
5
+ import subprocess
6
+ import sys
7
+
8
+ from qdir import Queue, default, parser
9
+
10
+ dprefix = os.environ.get(
11
+ "QDIR_DPREFIX",
12
+ os.environ.get("PBS_DPREFIX", "(#|@?(?i:rem ))(QDIR|PBS)"),
13
+ ) # regular expression prefix used to recognize optional file directives
14
+
15
+ parser.description = __doc__
16
+ default.name = os.environ.get("PBS_JOBNAME")
17
+ parser.add_argument(
18
+ "-N",
19
+ "--name",
20
+ help="Name for the job. Default is cmd[-1]",
21
+ default=default.name,
22
+ )
23
+ try:
24
+ default.interval = max(1, int(os.environ.get("QDIR_POLL_INTERVAL", "5")))
25
+ except ValueError:
26
+ default.interval = 5
27
+ parser.add_argument(
28
+ "-c",
29
+ "--interval",
30
+ type=int,
31
+ help="Polling interval in seconds. "
32
+ f"Default is environment variable QDIR_POLL_INTERVAL or {default.interval}",
33
+ default=default.interval,
34
+ )
35
+ parser.add_argument(
36
+ "-f",
37
+ "--foreground",
38
+ action="store_true",
39
+ help="Prevent qsub from forking to wait in background.",
40
+ default=False,
41
+ )
42
+ default.output = os.environ.get("QDIR_LOGNAME", "{name}._{jobId}")
43
+ parser.add_argument(
44
+ "-o",
45
+ "--output",
46
+ help="File name for combined stdout and stderr. "
47
+ f"Default is environment variable QDIR_LOGNAME or {default.output!r}",
48
+ default=default.output,
49
+ )
50
+ default.status = os.environ.get(
51
+ "QDIR_STATUS", "{jobId} {name} {status} after {duration}"
52
+ )
53
+ parser.add_argument(
54
+ "-s",
55
+ "--status",
56
+ help="Specify the format of job status messages output to terminal. "
57
+ f"Default is environment variable QDIR_STATUS or {default.status!r}",
58
+ default=default.status,
59
+ )
60
+ parser.add_argument(
61
+ "cmd",
62
+ nargs="+",
63
+ help="The command to enqueue plus any optional arguments. "
64
+ "This command must be in your PATH or explicitly specified. ",
65
+ )
66
+
67
+
68
+ def parseArguments(argv: list[str]):
69
+ """Parse the given list of arguments
70
+
71
+ Uses commandline arguments for top priority followed by in-file directives
72
+ and lastly default values.
73
+
74
+ In-file directives are the same format as commandline options and should
75
+ occur in comment lines at the beginning of the job script.
76
+ Directive lines and are preceeded by the dprefix regular expression.
77
+ """
78
+
79
+ import re
80
+ import shlex
81
+
82
+ # Parse commandline arguments to find cmd
83
+ argvNamespace = parser.parse_args(argv)
84
+
85
+ # Scan cmd[0] for optional file directives
86
+ directives = []
87
+ dprefixRe = re.compile(dprefix)
88
+ commentRe = re.compile("(#|@?(?i:rem|echo))")
89
+ try:
90
+ with open(argvNamespace.cmd[0]) as jobScript:
91
+ for line in jobScript:
92
+ m = dprefixRe.match(line)
93
+ if m:
94
+ directives.extend(shlex.split(line[m.end() :]))
95
+ elif len(line.strip()) == 0:
96
+ continue # ignore blank lines
97
+ elif commentRe.match(line):
98
+ continue # ignore non-directive comment lines
99
+ else:
100
+ break # otherwise stop processing
101
+ except OSError:
102
+ pass
103
+ directives.append("cmd")
104
+ _, unknown = parser.parse_known_args(directives)
105
+
106
+ # Finally, parse the combined options with priority for argv
107
+ options, _ = parser.parse_known_args(directives + argv)
108
+
109
+ # The cmd may only come from argv
110
+ options.cmd = argvNamespace.cmd
111
+
112
+ if not options.name and len(options.cmd) > 0:
113
+ options.name = options.cmd[-1] # default name
114
+ if unknown and options.verbose >= 2:
115
+ print("WARNING: Unknown file directives", *unknown, file=sys.stderr)
116
+ if options.verbose >= 5:
117
+ print("Arguments", argv, file=sys.stderr)
118
+ print("Directives", directives, file=sys.stderr)
119
+ print("options", options, file=sys.stderr)
120
+ return options
121
+
122
+
123
+ def qsub(argv):
124
+ """Enqueue a new command"""
125
+ options = parseArguments(argv[1:])
126
+ if not options.foreground:
127
+ cmd = [sys.executable, "-m", "qdir.qsub", "-f"] + argv[1:]
128
+ subprocess.Popen(cmd, start_new_session=True) # re-run in background
129
+ else:
130
+ queue = Queue(**vars(options))
131
+ queue.enqueue(options.cmd)
132
+
133
+
134
+ def main():
135
+ qsub(sys.argv)
136
+
137
+
138
+ if __name__ == "__main__":
139
+ main()
@@ -0,0 +1,127 @@
1
+ Metadata-Version: 2.4
2
+ Name: qdir
3
+ Version: 1.0.0
4
+ Summary: A very low overhead first-in, first-out job queue for the local machine
5
+ Author-email: Carl Osterwisch <costerwi@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/costerwi/qdir
8
+ Project-URL: Issues, https://github.com/costerwi/qdir/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.6
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: filelock
15
+ Requires-Dist: pywin32; sys_platform == "win32"
16
+ Dynamic: license-file
17
+
18
+ # qdir
19
+ A very low overhead first-in, first-out job queue for the local machine
20
+
21
+ The queue is maintained as a directory of tiny job status files.
22
+ When a new job is submitted, it will identify any existing status files and then add its own to the directory.
23
+ The job will not launch its command until all the preceeding status files are gone.
24
+ Each job will delete its own status file upon completion so the next job can begin.
25
+ There is a polling interval (default 5 seconds)
26
+ when the job checks to see if its predecessors have finished before launching its command.
27
+ A shared queue directory can be specified on a multi-user system to coordinate
28
+ job execution.
29
+ On Linux this is `/tmp` by default.
30
+
31
+ Any command may be submitted to the job queue using qsub
32
+ (see [usage](#usage) below).
33
+ There are also tools to check the status of the queue (qstat) and remove jobs (qdel).
34
+
35
+ # Installation
36
+ Requires python3 on Windows, Linux, or MacOS.
37
+ One way to get python3 on Windows is through the [Microsoft Store](https://apps.microsoft.com/detail/9PNRBTZXMB4Z).
38
+ Here is the command to install qdir once python3 is available:
39
+ ```bash
40
+ python3 -m pip install qdir
41
+ ```
42
+
43
+ See [here](https://pip.pypa.io/en/stable/installation/) if there is "no module named pip."
44
+
45
+ If there is a warning that the script directory is not in the PATH then
46
+ it must be added in order to use the command line tools.
47
+ Otherwise, [see below](#usage-without-script-directory-in-path).
48
+
49
+ ## Abaqus Finite Element Analysis (FEA) extras
50
+ The package includes additional files to help Abaqus users submit and monitor the qdir job queue.
51
+ Use the following command to configure Abaqus after qdir itself has been installed:
52
+ ```bash
53
+ python3 -m qdir.abaqus install
54
+ ```
55
+
56
+ By default, the necessary settings are appended to the end of the user `abaqus_v6.env` file.
57
+ The following command may be used to remove qdir settings for Abaqus:
58
+ ```bash
59
+ python3 -m qdir.abaqus uninstall
60
+ ```
61
+
62
+ # Usage
63
+ Below is a brief overview of usage.
64
+ The command line tools accept a `-h` or `--help` argument to list their full options.
65
+
66
+ ## Command line job submission
67
+ Submit commands or job scripts to the queue using the `qsub` command.
68
+ For example, on Linux:
69
+ ```bash
70
+ qsub ./myJobFile.sh
71
+ ```
72
+ or on Windows:
73
+ ```bash
74
+ qsub myJobFile.cmd
75
+ ```
76
+
77
+ Any printed output from the command will be saved to a file based on the job name with the job Id appended to the end.
78
+
79
+ Display command options with `qsub -h`
80
+
81
+ ## Command line queue status
82
+ Check status with the `qstat` command, for example:
83
+ ```bash
84
+ qstat
85
+ ```
86
+
87
+ ## Command line job removal
88
+ First use the `qstat` command above to find the job Ids and job names in the queue.
89
+ Delete or kill jobs with the `qdel` command followed by its jobId or job name.
90
+ For example, to delete job 12343:
91
+ ```bash
92
+ qdel 12343
93
+ ```
94
+
95
+ ## Usage without script directory in PATH
96
+ The `qsub`, `qstat`, and `qdel` scripts are shortcuts to call python3 methods.
97
+ These scripts will not be available at the command line if their directory is not in the PATH.
98
+ It's most convenient to add the directory to the PATH but it is also
99
+ possible to run the methods directly with some additional typing,
100
+ such as `python3 -m qdir.qstat` to execute `qstat`
101
+
102
+ ## Abaqus extras
103
+ Submit Abaqus jobs as usual from the command line:
104
+ ```bash
105
+ abaqus job=whatever
106
+ ```
107
+ Internally this will invoke the `qdir_submit.py` module to create a job script
108
+ and submit it to qdir.
109
+
110
+ You may submit jobs within Abaqus CAE by selecting `qdir` as the Job's queue before submitting.
111
+ You may check the queue status within CAE in the menu `Plug-ins > QDir Job Queue Monitor...`
112
+ Click on a Job to see more detailed information.
113
+ Right-click on a Job to expose the menu allowing you to delete the Job.
114
+
115
+ ## Customization
116
+ Most parameters used by qdir may be customized with command line options,
117
+ system environment variables, or directives within the job script itself.
118
+
119
+ | Option | Description | Default |
120
+ |--------|-------------|---------|
121
+ | -N *NAME*, --name *NAME* | Name for the job. | The last argument of the command |
122
+ | -q *QUEUE*, --queue *QUEUE* | Directory to store the queue files. | environment variable `QDIR_QUEUE` or `/tmp` on Linux |
123
+ | -c *INTERVAL*, --interval *INTERVAL* | Polling interval in seconds. | environment variable `QDIR_POLL_INTERVAL` or 5 |
124
+ | -i *INFO*, --info *INFO* | Specify the format of information about jobs in queue | environment variable `QDIR_INFO` or `{jobId:8} {owner:12} {status:35} {name}` |
125
+ | -o *OUTPUT*, --output *OUTPUT* | File name for combined stdout and stderr. | environment variable `QDIR_LOGNAME` or `{name}._{jobId}`
126
+ | -s *STATUS*, --status *STATUS* | Specify the format of job status messages output to terminal. | environment variable `QDIR_STATUS` or `{jobId} {name} {status} after {duration}`
127
+ | -v *VERBOSE*, --verbose *VERBOSE* | Set the verbosity level. | environment variable `QDIR_VERBOSE` or 2 |
@@ -0,0 +1,14 @@
1
+ qdir/__init__.py,sha256=J10e2VNPnnB4oGLXQVyWaK2DAoR6DmPN3I90QhPNO60,11652
2
+ qdir/qdel.py,sha256=vjDD_zBhtu4R8KkWXMNtxZg6KMXlRU8vO2gEFGHqC7k,1440
3
+ qdir/qstat.py,sha256=8vQbhoR8MEtxmKiuOEAIqJEoPwV7PpSoW_JaYsrNRPg,4306
4
+ qdir/qsub.py,sha256=0lxm3WAfewV4ebZYufQROuoEubsA3Noq2qL33c6kbnk,4182
5
+ qdir/abaqus/__init__.py,sha256=AqfK9oYkq0oSbXHb-2wZmL5KxbKcvsVy4DS3wbzvkbc,113
6
+ qdir/abaqus/__main__.py,sha256=93NgqW2IAntLmPFFewBdFXnTpMLolm3yfkqbIQDWhws,2034
7
+ qdir/abaqus/qdir_plugin.py,sha256=KswqqnuaT6fuA247tT7mVD7nf_VVHsecuf7mYxH7peQ,8212
8
+ qdir/abaqus/qdir_submit.py,sha256=P4Q7g-WhucTFzT9Y1SEEHA4NX1b8vmhoaLV8INzOaSQ,5435
9
+ qdir-1.0.0.dist-info/licenses/LICENSE,sha256=MuZbvuvcRPehx5RAJMdgAuV0yvzxWaPcQGKPeihaIZY,1072
10
+ qdir-1.0.0.dist-info/METADATA,sha256=DwYy2l2kU6PTspAZS7YGJRp79tPPL8zDCx4kgVpI5mc,5430
11
+ qdir-1.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
12
+ qdir-1.0.0.dist-info/entry_points.txt,sha256=zBcB1NgRBXkzcAWnk9jHgHhn1Mk2Yo-oFJFBEWymw8A,86
13
+ qdir-1.0.0.dist-info/top_level.txt,sha256=fRgcgmSYWhMIV8wvfnG7YYMu5qS8PfIabbjXLe6Cj9E,5
14
+ qdir-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ qdel = qdir.qdel:main
3
+ qstat = qdir.qstat:main
4
+ qsub = qdir.qsub:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Carl Osterwisch
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ qdir