python-dmon 0.2.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.
- dmon/__init__.py +0 -0
- dmon/__main__.py +5 -0
- dmon/cli.py +232 -0
- dmon/config.py +195 -0
- dmon/constants.py +14 -0
- dmon/control.py +542 -0
- dmon/py.typed +0 -0
- dmon/runner.py +188 -0
- dmon/types.py +63 -0
- dmon/utils.py +32 -0
- python_dmon-0.2.0.dist-info/METADATA +156 -0
- python_dmon-0.2.0.dist-info/RECORD +14 -0
- python_dmon-0.2.0.dist-info/WHEEL +4 -0
- python_dmon-0.2.0.dist-info/entry_points.txt +3 -0
dmon/runner.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
import logging
|
|
4
|
+
from logging.handlers import RotatingFileHandler
|
|
5
|
+
import os
|
|
6
|
+
import signal
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("dmon.runner")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FixedSizeRotatingFileHandler(RotatingFileHandler):
|
|
15
|
+
"""
|
|
16
|
+
Custom RotatingFileHandler that renames the old log file with a timestamp suffix
|
|
17
|
+
instead of deleting it.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, filename, maxBytes):
|
|
21
|
+
super().__init__(filename, maxBytes=maxBytes)
|
|
22
|
+
|
|
23
|
+
def doRollover(self):
|
|
24
|
+
if self.stream:
|
|
25
|
+
self.stream.close()
|
|
26
|
+
# Create timestamped filename for the rollover logs
|
|
27
|
+
current_time = datetime.now().strftime(".%Y%m%d-%H:%M:%S")
|
|
28
|
+
self.rotate(self.baseFilename, self.baseFilename + current_time)
|
|
29
|
+
self.stream = self._open()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_file_dir(file_path):
|
|
33
|
+
return os.path.dirname(file_path)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def make_dir(dir):
|
|
37
|
+
dir = os.path.abspath(dir)
|
|
38
|
+
if not os.path.exists(dir):
|
|
39
|
+
os.makedirs(dir, exist_ok=True)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def make_file_dir(file_path):
|
|
43
|
+
par_dir = get_file_dir(file_path)
|
|
44
|
+
make_dir(par_dir)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def need_rotate(log_path, max_log_size):
|
|
48
|
+
return os.path.getsize(log_path) >= max_log_size and max_log_size > 0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def rotate_log(log_path):
|
|
52
|
+
current_time = datetime.now().strftime(".%Y%m%d-%H%M%S")
|
|
53
|
+
new_name = log_path + current_time
|
|
54
|
+
logger.info(f"Rotating {log_path} to {new_name}")
|
|
55
|
+
if os.path.exists(new_name):
|
|
56
|
+
logger.warning(f"{new_name} already exists, skip renaming")
|
|
57
|
+
else:
|
|
58
|
+
make_file_dir(new_name)
|
|
59
|
+
os.rename(log_path, new_name)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def loop_to_log(bin_fd, log_path, max_log_size):
|
|
63
|
+
while True: # loop once whenever need to rotate
|
|
64
|
+
with open(log_path, "ab") as log_file:
|
|
65
|
+
while True:
|
|
66
|
+
try:
|
|
67
|
+
# read byte by byte in binary mode to avoid decoding
|
|
68
|
+
# error caused by utf8 character truncation
|
|
69
|
+
byte = bin_fd.read(1)
|
|
70
|
+
if byte:
|
|
71
|
+
log_file.write(byte)
|
|
72
|
+
log_file.flush() # immediately write the content to the file
|
|
73
|
+
# When a newline character is read, check if
|
|
74
|
+
# rotation is needed; if so, break the loop
|
|
75
|
+
if byte == b"\n" and need_rotate(log_path, max_log_size):
|
|
76
|
+
break
|
|
77
|
+
else:
|
|
78
|
+
logger.info("Read EOF, now closing...")
|
|
79
|
+
return
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logger.exception(f"Exception in while loop: {e}")
|
|
82
|
+
rotate_log(log_path) # rotate log file
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def catch_exception(func):
|
|
86
|
+
def wrapper(*args, **kwargs):
|
|
87
|
+
try:
|
|
88
|
+
return func(*args, **kwargs)
|
|
89
|
+
except Exception as e:
|
|
90
|
+
logger.exception(f"Exception in {func.__name__}: {e}")
|
|
91
|
+
|
|
92
|
+
return wrapper
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@catch_exception
|
|
96
|
+
def main(
|
|
97
|
+
cmd,
|
|
98
|
+
log_path,
|
|
99
|
+
max_log_size,
|
|
100
|
+
rotate_log_path,
|
|
101
|
+
max_rotate_log_size,
|
|
102
|
+
):
|
|
103
|
+
# Configure logging
|
|
104
|
+
rh = None
|
|
105
|
+
if rotate_log_path:
|
|
106
|
+
rh = FixedSizeRotatingFileHandler(rotate_log_path, maxBytes=max_rotate_log_size)
|
|
107
|
+
logging.basicConfig(
|
|
108
|
+
format="%(asctime)s.%(msecs)03d - %(process)d - %(levelname)s - %(message)s",
|
|
109
|
+
level=logging.INFO,
|
|
110
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
111
|
+
handlers=[rh] if rh else None,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
logger.info(
|
|
115
|
+
f"Prepare for rotating logs: {log_path=} {max_log_size=} {rotate_log_path=} {max_rotate_log_size=}"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
shell = isinstance(cmd, str)
|
|
119
|
+
# Start the child process with stdout/stderr redirected to the log
|
|
120
|
+
# env will be inherited from parent process
|
|
121
|
+
# cwd will be inherited from parent process
|
|
122
|
+
proc = subprocess.Popen(
|
|
123
|
+
cmd,
|
|
124
|
+
stdout=subprocess.PIPE,
|
|
125
|
+
stderr=subprocess.STDOUT,
|
|
126
|
+
shell=shell,
|
|
127
|
+
text=False, # binary mode
|
|
128
|
+
bufsize=0, # unbuffered
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# register signal handler to terminate the child process
|
|
132
|
+
def signal_handler(signum, frame):
|
|
133
|
+
logger.info(f"Received signal {signum}, forwarding to child process...")
|
|
134
|
+
# proc.terminate()
|
|
135
|
+
proc.send_signal(signum)
|
|
136
|
+
logger.info("Waiting for child process to exit...")
|
|
137
|
+
proc.wait()
|
|
138
|
+
logger.info("Child process exited, all done.")
|
|
139
|
+
sys.exit(0)
|
|
140
|
+
|
|
141
|
+
# Set up signal handlers
|
|
142
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
143
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
144
|
+
|
|
145
|
+
logger.info(f"Started process {proc.pid} with command: {cmd} (shell={shell})")
|
|
146
|
+
|
|
147
|
+
make_file_dir(log_path)
|
|
148
|
+
loop_to_log(proc.stdout, log_path, max_log_size)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
if __name__ == "__main__":
|
|
152
|
+
parser = argparse.ArgumentParser(
|
|
153
|
+
description="Dmon task runner with log rotation utility",
|
|
154
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
155
|
+
)
|
|
156
|
+
parser.add_argument(
|
|
157
|
+
"command",
|
|
158
|
+
nargs=argparse.ONE_OR_MORE,
|
|
159
|
+
help="Command with arguments to run",
|
|
160
|
+
)
|
|
161
|
+
parser.add_argument("--shell", action="store_true", help="Run command in shell")
|
|
162
|
+
parser.add_argument("--log-path", help="Log file path", required=True)
|
|
163
|
+
parser.add_argument(
|
|
164
|
+
"--max-log-size",
|
|
165
|
+
help="Max log file size (MB); 0 for no rotation",
|
|
166
|
+
type=int,
|
|
167
|
+
default=5,
|
|
168
|
+
)
|
|
169
|
+
parser.add_argument(
|
|
170
|
+
"--rotate-log-path",
|
|
171
|
+
help="Path of the log output of this rotation process; if not provided, output to stdout",
|
|
172
|
+
default=None,
|
|
173
|
+
)
|
|
174
|
+
parser.add_argument(
|
|
175
|
+
"--max-rotate-log-size",
|
|
176
|
+
help="Max file size (MB) for this rotation log; 0 for no rotation",
|
|
177
|
+
type=int,
|
|
178
|
+
default=5,
|
|
179
|
+
)
|
|
180
|
+
args = parser.parse_args()
|
|
181
|
+
main(
|
|
182
|
+
" ".join(args.command) if args.shell else args.command,
|
|
183
|
+
args.log_path,
|
|
184
|
+
args.max_log_size * 1024 * 1024,
|
|
185
|
+
args.rotate_log_path,
|
|
186
|
+
args.max_rotate_log_size * 1024 * 1024,
|
|
187
|
+
)
|
|
188
|
+
logger.info("Process finished.")
|
dmon/types.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from dataclasses import asdict, dataclass, field
|
|
2
|
+
import json
|
|
3
|
+
from os import PathLike
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Dict, List, Optional, Union
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
if sys.version_info >= (3, 9):
|
|
10
|
+
PathType = Union[str, PathLike[str]]
|
|
11
|
+
else:
|
|
12
|
+
PathType = Union[str, PathLike]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
CmdType = Union[str, List[str]]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class DmonTaskConfig:
|
|
20
|
+
name: str = ""
|
|
21
|
+
"""Name of the task"""
|
|
22
|
+
cmd: CmdType = ""
|
|
23
|
+
"""Command to run, either a string (for shell) or a list of strings (for exec)"""
|
|
24
|
+
cwd: str = ""
|
|
25
|
+
"""Working directory to run the command in"""
|
|
26
|
+
env: Dict[str, str] = field(default_factory=dict)
|
|
27
|
+
"""Environment variables to set for the command"""
|
|
28
|
+
override_env: bool = False
|
|
29
|
+
"""Whether to override the entire environment with the provided env"""
|
|
30
|
+
log_path: str = ""
|
|
31
|
+
"""Path to log file"""
|
|
32
|
+
log_rotate: bool = False
|
|
33
|
+
"""Whether to rotate log file"""
|
|
34
|
+
log_max_size: int = 5
|
|
35
|
+
"""Size in MB to rotate log file"""
|
|
36
|
+
rotate_log_path: str = ""
|
|
37
|
+
"""Path to rotation log file"""
|
|
38
|
+
rotate_log_max_size: int = 5
|
|
39
|
+
"""Size in MB to rotation log file"""
|
|
40
|
+
meta_path: str = ""
|
|
41
|
+
"""Path to meta file"""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class DmonMeta(DmonTaskConfig):
|
|
46
|
+
pid: int = -1
|
|
47
|
+
shell: bool = False
|
|
48
|
+
popen_kwargs: Dict = field(default_factory=dict)
|
|
49
|
+
create_time: float = -1
|
|
50
|
+
create_time_human: str = "N/A"
|
|
51
|
+
|
|
52
|
+
def dump(self, path: PathType):
|
|
53
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
54
|
+
json.dump(asdict(self), f, indent=2, ensure_ascii=False)
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def load(path: PathType) -> Optional["DmonMeta"]:
|
|
58
|
+
p = Path(path)
|
|
59
|
+
if p.exists():
|
|
60
|
+
with p.open("r", encoding="utf-8") as f:
|
|
61
|
+
data = json.load(f)
|
|
62
|
+
return DmonMeta(**data)
|
|
63
|
+
return None
|
dmon/utils.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
ANSI_RE = re.compile(r"\x1b\[[0-9;]*m")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def len_ansi(s: str) -> int:
|
|
9
|
+
return len(ANSI_RE.sub("", s))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def pad_ansi(
|
|
13
|
+
s: str, width: int, align: Literal["<", ">", "^"] = "<", fill: str = " "
|
|
14
|
+
) -> str:
|
|
15
|
+
"""Pad a string with ANSI escape codes to a given width."""
|
|
16
|
+
real_len = len_ansi(s)
|
|
17
|
+
if real_len == width:
|
|
18
|
+
return s
|
|
19
|
+
if real_len > width:
|
|
20
|
+
return s[: width - 3] + "..." # truncate and add ellipsis
|
|
21
|
+
|
|
22
|
+
pad_len = width - real_len
|
|
23
|
+
if align == "<":
|
|
24
|
+
return s + fill * pad_len
|
|
25
|
+
elif align == ">":
|
|
26
|
+
return fill * pad_len + s
|
|
27
|
+
elif align == "^":
|
|
28
|
+
left = pad_len // 2
|
|
29
|
+
right = pad_len - left
|
|
30
|
+
return fill * left + s + fill * right
|
|
31
|
+
else:
|
|
32
|
+
raise ValueError(f"Invalid align: {align}")
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: python-dmon
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A lightweight, cross-platform daemon manager that runs any command as a background process.
|
|
5
|
+
Keywords: python-dmon,dmon,daemon,background,detach,process management
|
|
6
|
+
Author: Atomie CHEN
|
|
7
|
+
Author-email: Atomie CHEN <atomic_cwh@163.com>
|
|
8
|
+
Requires-Dist: colorama>=0.4.6
|
|
9
|
+
Requires-Dist: psutil>=7.1.0
|
|
10
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
11
|
+
Requires-Dist: termcolor>=2.4.0
|
|
12
|
+
Requires-Dist: tomli>=2.2.1 ; python_full_version < '3.11'
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Project-URL: Bug Tracker, https://github.com/atomiechen/python-dmon/issues
|
|
15
|
+
Project-URL: Changelog, https://github.com/atomiechen/python-dmon/blob/master/CHANGELOG.md
|
|
16
|
+
Project-URL: Homepage, https://github.com/atomiechen/python-dmon
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# python-dmon
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
[](https://github.com/atomiechen/python-dmon)
|
|
23
|
+
[](https://pypi.org/project/python-dmon/)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
A lightweight, cross-platform daemon manager that runs any command — called a *task* — as a background process.
|
|
27
|
+
It also supports logging and log rotation out of the box.
|
|
28
|
+
**No Docker or extra dependencies required**.
|
|
29
|
+
|
|
30
|
+
Shipped as the CLI tool `dmon`.
|
|
31
|
+
It is a Python-based and more powerful successor to the [handy-backend shell scripts](https://github.com/atomiechen/handy-backend).
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
- 🖥️ **Cross-platform:** Works on Linux, macOS, and Windows.
|
|
37
|
+
- ⚡ **Lightweight:** Pure Python, no Docker or external dependencies needed.
|
|
38
|
+
- 🧩 **Flexible tasks:** Tasks can be configured in `pyproject.toml` or `dmon.yaml`; or run ad-hoc commands directly.
|
|
39
|
+
- 🪵 **Logging & log rotation:** Automatically manage log files to prevent uncontrolled growth.
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
pip install python-dmon
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
To get the latest features, install from source:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
pip install git+https://github.com/atomiechen/python-dmon.git
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Getting Started
|
|
55
|
+
|
|
56
|
+
### Prepare Configuration
|
|
57
|
+
|
|
58
|
+
Create a `dmon.yaml` file:
|
|
59
|
+
|
|
60
|
+
```yaml
|
|
61
|
+
tasks:
|
|
62
|
+
app: ["python", "-u", "server.py"]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Or add to your `pyproject.toml`:
|
|
66
|
+
|
|
67
|
+
```toml
|
|
68
|
+
[tool.dmon.tasks]
|
|
69
|
+
app = ["python", "-u", "server.py"]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Commands can be a single string (run in shell), or list of strings (exec form).
|
|
73
|
+
See [Example Task Configuration](#example-task-configuration) for more configuration options.
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
### Run tasks
|
|
77
|
+
|
|
78
|
+
Run a configured task by its name:
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
# Start a task
|
|
82
|
+
dmon start app
|
|
83
|
+
|
|
84
|
+
# Stop a running task
|
|
85
|
+
dmon stop app
|
|
86
|
+
|
|
87
|
+
# Check task status
|
|
88
|
+
dmon status app
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
If only one task is defined in the config file, you can omit the task name:
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
dmon start
|
|
95
|
+
dmon stop
|
|
96
|
+
dmon status
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
### Run an ad-hoc command
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
# Run a command with arguments in the background
|
|
104
|
+
dmon run --name myserver python -u server.py
|
|
105
|
+
|
|
106
|
+
# Run a shell command in the background
|
|
107
|
+
dmon run --shell echo "Hello World"
|
|
108
|
+
|
|
109
|
+
# Run a shell script in the background
|
|
110
|
+
dmon run --cwd /path/to/script bash myscript.sh
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
> [!NOTE]
|
|
114
|
+
> If no name is provided, `dmon` automatically assigns a fixed task name `default_run` to prevent duplicate runs.
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
### List all running tasks
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
dmon list
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
## Example Task Configuration
|
|
125
|
+
|
|
126
|
+
A task can be a **string**, **list**, or **dictionary**.
|
|
127
|
+
|
|
128
|
+
Here is a more complete example with default values:
|
|
129
|
+
|
|
130
|
+
```yaml
|
|
131
|
+
tasks:
|
|
132
|
+
your_task_name:
|
|
133
|
+
# Command to run; can be a string (run in shell) or list of strings (exec form)
|
|
134
|
+
cmd: ["python", "server.py"] # required
|
|
135
|
+
cwd: "/path/to/working/dir" # (default: current dir)
|
|
136
|
+
env: # (default: inherit from parent process)
|
|
137
|
+
PYTHONUNBUFFERED: "1"
|
|
138
|
+
override_env: false # override parent env and only use env defined here
|
|
139
|
+
log_path: "logs/<name>.log" # path to log file
|
|
140
|
+
log_rotate: false # enable log rotation
|
|
141
|
+
log_max_size: 5 # max log file size before rotation in MB
|
|
142
|
+
rotate_log_path: "logs/<name>.rotate.log" # path to rotation log
|
|
143
|
+
rotate_log_max_size: 5 # max rotation log file size in MB
|
|
144
|
+
meta_path: ".dmon/<name>.meta.json" # path to meta file
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Under the Hood
|
|
148
|
+
|
|
149
|
+
Each task is associated with a meta file (e.g. `.dmon/<name>.meta.json`) stored in the current working directory.
|
|
150
|
+
The file contains details such as the command, PID, log path, and more.
|
|
151
|
+
**Do not** modify or delete these files manually.
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
[python-dmon](https://github.com/atomiechen/python-dmon) © 2025 by [Atomie CHEN](https://github.com/atomiechen) is licensed under the [MIT License](https://github.com/atomiechen/python-dmon/blob/main/LICENSE).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
dmon/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
dmon/__main__.py,sha256=wu5N2wk8mvBgyvr2ghmQf4prezAe0_i-p123VVreyYc,62
|
|
3
|
+
dmon/cli.py,sha256=IVJ5jCHkaLGTQjMmWCuJbhnt4MWEOaNWQajC-mm3LUQ,7599
|
|
4
|
+
dmon/config.py,sha256=COzwN0Ms8q32pV6dqsZU59RFE2GQmqyAmfygDotc4CA,6610
|
|
5
|
+
dmon/constants.py,sha256=y5S532iTeklaR28BFbvpdOHFj1udbEJ-reD1mTCcC6E,351
|
|
6
|
+
dmon/control.py,sha256=0_NrrOA0sUM_lFHNyKGAmQsBbJhXwXfj8kxOp2S8Ni8,16311
|
|
7
|
+
dmon/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
dmon/runner.py,sha256=zEW7TnHMfIw6g3UTjOTUcLFKueXpqSmNDP3ZOptXQKs,5892
|
|
9
|
+
dmon/types.py,sha256=ZtTAK-cfs2MgUFRwDH61zeIU2B4dHZDbx1644tYI1h4,1795
|
|
10
|
+
dmon/utils.py,sha256=zVQC9YNhMQu4hrAp9k2ds2egnbWZZv7S5w8i2oVIPJg,808
|
|
11
|
+
python_dmon-0.2.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
12
|
+
python_dmon-0.2.0.dist-info/entry_points.txt,sha256=rad5ugz4K_IyNXS_IrFPu0tS-UXlBCSJRhJv-OWd_yk,45
|
|
13
|
+
python_dmon-0.2.0.dist-info/METADATA,sha256=s1AoObf2NB_kK_Era10wpm0X6OwH-J0UVeAS8Dg6pcs,4498
|
|
14
|
+
python_dmon-0.2.0.dist-info/RECORD,,
|