pymdkit 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.
@@ -0,0 +1,62 @@
1
+ """Write VASP relaxation inputs (pymatgen MPRelaxSet) for a structure.
2
+
3
+ Single file: vasp-relax -i model.vasp
4
+ Whole folder: vasp-relax -if optimal_occupancy # one ./<name>/ job per structure
5
+ Trajectory: vasp-relax -it traj.xyz # one ./frame_N/ job per frame
6
+ Custom INCAR: vasp-relax -if optimal_occupancy -custom-setting my_settings.txt
7
+
8
+ VASP jobs are always individual (one structure per folder). The defaults below
9
+ can be overridden with a -custom-setting file.
10
+ """
11
+
12
+ import argparse
13
+
14
+ from pymatgen.io.vasp.sets import MPRelaxSet
15
+
16
+ try:
17
+ from . import _vaspset
18
+ except ImportError: # running as a standalone script
19
+ import _vaspset
20
+
21
+ COMMAND = "vasp-relax"
22
+ HELP = "Write VASP relaxation inputs for a structure (file, folder, or trajectory)."
23
+
24
+ # Default INCAR settings for a relaxation run.
25
+ DEFAULT_SETTINGS = {
26
+ "ALGO": "Normal",
27
+ "EDIFF": "1e-05",
28
+ "EDIFFG": "-0.02",
29
+ "ENCUT": "520.0",
30
+ "GGA": "PE",
31
+ "KGAMMA": "True",
32
+ "KSPACING": "0.4",
33
+ "LASPH": "True",
34
+ "LCHARG": "False",
35
+ "LREAL": "Auto",
36
+ "LWAVE": "False",
37
+ "NELM": "120",
38
+ "NSW": "300",
39
+ "ISIF": "3",
40
+ "IBRION": "2",
41
+ "PREC": "Normal",
42
+ "ISMEAR": "0",
43
+ "SIGMA": "0.05",
44
+ "LMAXMIX": None,
45
+ "MAGMOM": None,
46
+ "ISPIN": "1",
47
+ "LORBIT": None,
48
+ }
49
+
50
+
51
+ def add_arguments(parser):
52
+ _vaspset.add_vasp_arguments(parser)
53
+
54
+
55
+ def run(args):
56
+ return _vaspset.run_vasp_set(args, MPRelaxSet, DEFAULT_SETTINGS)
57
+
58
+
59
+ if __name__ == "__main__":
60
+ _p = argparse.ArgumentParser(description=__doc__)
61
+ add_arguments(_p)
62
+ raise SystemExit(run(_p.parse_args()))
@@ -0,0 +1,59 @@
1
+ """Write VASP static / single-point inputs (pymatgen MPStaticSet) for a structure.
2
+
3
+ Single file: vasp-static -i model.vasp
4
+ Whole folder: vasp-static -if optimal_occupancy # one ./<name>/ job per structure
5
+ Trajectory: vasp-static -it export_FPS_Filter_structure.xyz # one ./frame_N/ job per frame
6
+ Custom INCAR: vasp-static -it traj.xyz -custom-setting my_settings.txt
7
+
8
+ VASP jobs are always individual (one structure per folder). The defaults below
9
+ (a single-point NSW=0 run) can be overridden with a -custom-setting file.
10
+ """
11
+
12
+ import argparse
13
+
14
+ from pymatgen.io.vasp.sets import MPStaticSet
15
+
16
+ try:
17
+ from . import _vaspset
18
+ except ImportError: # running as a standalone script
19
+ import _vaspset
20
+
21
+ COMMAND = "vasp-static"
22
+ HELP = "Write VASP static (single-point) inputs for a structure (file, folder, or trajectory)."
23
+
24
+ # Default INCAR settings for a static / single-point run.
25
+ DEFAULT_SETTINGS = {
26
+ "ALGO": "Normal",
27
+ "EDIFF": "1e-05",
28
+ "ENCUT": "520.0",
29
+ "GGA": "PE",
30
+ "KGAMMA": "True",
31
+ "KSPACING": "0.4",
32
+ "LASPH": "True",
33
+ "LCHARG": "False",
34
+ "LREAL": "Auto",
35
+ "LWAVE": "False",
36
+ "NELM": "120",
37
+ "NSW": "0",
38
+ "MAGMOM": None,
39
+ "LMAXMIX": None,
40
+ "LORBIT": None,
41
+ "ISPIN": "1",
42
+ "PREC": "Normal",
43
+ "ISMEAR": "0",
44
+ "SIGMA": "0.05",
45
+ }
46
+
47
+
48
+ def add_arguments(parser):
49
+ _vaspset.add_vasp_arguments(parser)
50
+
51
+
52
+ def run(args):
53
+ return _vaspset.run_vasp_set(args, MPStaticSet, DEFAULT_SETTINGS)
54
+
55
+
56
+ if __name__ == "__main__":
57
+ _p = argparse.ArgumentParser(description=__doc__)
58
+ add_arguments(_p)
59
+ raise SystemExit(run(_p.parse_args()))
@@ -0,0 +1,115 @@
1
+ """
2
+ pymdkit dispatcher.
3
+
4
+ Builds a single ``pymdkit`` command whose subcommands are auto-discovered from
5
+ the :mod:`pymdkit.commands` package. Each command module must define::
6
+
7
+ COMMAND : str # subcommand name, e.g. "msd"
8
+ HELP : str # one-line help string
9
+ def add_arguments(parser): ... # register the command's flags
10
+ def run(args) -> int: ... # do the work, return an exit code
11
+
12
+ Discovery is two-phase. Command *names* and *help* lines are read straight from
13
+ the source files via the ``ast`` module -- without importing them -- so listing
14
+ ``pymdkit --help`` never triggers a heavy ``import pymatgen`` / ``import ase``.
15
+ Only the single module you actually invoke is imported. A command whose optional
16
+ dependency is missing therefore fails only when you run it, never for the whole
17
+ CLI.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import argparse
23
+ import ast
24
+ import importlib
25
+ import pkgutil
26
+ import sys
27
+ from pathlib import Path
28
+ from typing import Dict, Tuple
29
+
30
+ from . import __version__, commands
31
+
32
+
33
+ def _read_meta(path: Path, modname: str) -> Tuple[str, str]:
34
+ """Read (command_name, help_text) from a source file without importing it."""
35
+ command, help_text = modname, ""
36
+ try:
37
+ tree = ast.parse(path.read_text(encoding="utf-8"))
38
+ except (OSError, SyntaxError):
39
+ return command, help_text
40
+
41
+ for node in tree.body:
42
+ if isinstance(node, ast.Assign):
43
+ for target in node.targets:
44
+ if (isinstance(target, ast.Name)
45
+ and isinstance(node.value, ast.Constant)
46
+ and isinstance(node.value.value, str)):
47
+ if target.id == "COMMAND":
48
+ command = node.value.value
49
+ elif target.id == "HELP":
50
+ help_text = node.value.value
51
+
52
+ if not help_text:
53
+ doc = (ast.get_docstring(tree) or "").strip()
54
+ help_text = doc.splitlines()[0] if doc else ""
55
+ return command, help_text
56
+
57
+
58
+ def _discover() -> Dict[str, Tuple[str, str]]:
59
+ """Return {command_name: (module_name, help_text)} for every command module."""
60
+ pkg_dir = Path(commands.__path__[0])
61
+ found: Dict[str, Tuple[str, str]] = {}
62
+ for info in pkgutil.iter_modules([str(pkg_dir)]):
63
+ if info.name.startswith("_"):
64
+ continue
65
+ command, help_text = _read_meta(pkg_dir / f"{info.name}.py", info.name)
66
+ found[command] = (info.name, help_text)
67
+ return found
68
+
69
+
70
+ def _build_top_parser(cmds: Dict[str, Tuple[str, str]]) -> argparse.ArgumentParser:
71
+ parser = argparse.ArgumentParser(
72
+ prog="pymdkit",
73
+ description="Unified CLI for atomistic / MD structure workflows.",
74
+ )
75
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
76
+ sub = parser.add_subparsers(dest="command", metavar="<command>")
77
+ for name in sorted(cmds):
78
+ sub.add_parser(name, help=cmds[name][1], add_help=False)
79
+ return parser
80
+
81
+
82
+ def main(argv=None) -> int:
83
+ argv = list(sys.argv[1:] if argv is None else argv)
84
+ cmds = _discover()
85
+ top = _build_top_parser(cmds)
86
+
87
+ # No command, or top-level help/version: let the top parser handle it.
88
+ if not argv:
89
+ top.print_help()
90
+ return 1
91
+ if argv[0] in ("-h", "--help", "--version"):
92
+ top.parse_args(argv) # prints help/version and exits
93
+ return 0
94
+
95
+ command = argv[0]
96
+ if command not in cmds:
97
+ top.error(f"invalid choice: {command!r} (choose from {', '.join(sorted(cmds))})")
98
+
99
+ # Phase 2: import only the chosen module and build its dedicated parser.
100
+ modname = cmds[command][0]
101
+ module = importlib.import_module(f"{commands.__name__}.{modname}")
102
+ cmd_parser = argparse.ArgumentParser(
103
+ prog=f"pymdkit {command}",
104
+ description=getattr(module, "HELP", None) or (module.__doc__ or "").strip(),
105
+ )
106
+ if hasattr(module, "add_arguments"):
107
+ module.add_arguments(cmd_parser)
108
+ args = cmd_parser.parse_args(argv[1:])
109
+
110
+ result = module.run(args)
111
+ return 0 if result is None else int(result)
112
+
113
+
114
+ if __name__ == "__main__":
115
+ raise SystemExit(main())
@@ -0,0 +1,201 @@
1
+ Metadata-Version: 2.4
2
+ Name: pymdkit
3
+ Version: 1.0.0
4
+ Summary: A unified command-line toolkit for atomistic / MD structure workflows.
5
+ Author-email: Yueda Wang <ydwang0608@ustc.edu.cn>
6
+ License-Expression: GPL-3.0-or-later
7
+ Keywords: materials-science,molecular-dynamics,vasp,gpumd,ase,pymatgen
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: Topic :: Scientific/Engineering :: Chemistry
12
+ Classifier: Topic :: Scientific/Engineering :: Physics
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: numpy
17
+ Requires-Dist: scipy
18
+ Requires-Dist: ase
19
+ Requires-Dist: pymatgen
20
+ Requires-Dist: mp_api
21
+ Requires-Dist: pyxtal
22
+ Requires-Dist: gemmi
23
+ Requires-Dist: tqdm
24
+ Dynamic: license-file
25
+
26
+ # pymdkit
27
+
28
+ A single command-line tool that bundles a collection of atomistic / molecular-dynamics
29
+ structure scripts behind one executable: `pymdkit`. Instead of copying individual
30
+ scripts into each working folder and running `python some_script.py`, you install
31
+ `pymdkit` once and call any tool from anywhere as `pymdkit <command> [options]`.
32
+
33
+ Every command exposes named `--flags` (no positional guessing), and each underlying
34
+ script is still runnable on its own.
35
+
36
+ ## Install ("compiling" the executable)
37
+
38
+ Python isn't compiled to a binary; the equivalent step is installing the package,
39
+ which creates the `pymdkit` command on your `PATH`.
40
+
41
+ ```bash
42
+ cd pymdkit # the folder containing pyproject.toml
43
+ pip install -e . # editable install: edits to scripts take effect immediately
44
+ ```
45
+
46
+ On an HPC cluster, activate your conda env / `module load` first so `pymdkit` lands in
47
+ that environment's `bin`. This installs every dependency (numpy, scipy, ase, pymatgen,
48
+ pyxtal, mp_api, gemmi, tqdm), so all commands work out of the box.
49
+
50
+ Verify:
51
+
52
+ ```bash
53
+ pymdkit --version
54
+ pymdkit --help # lists every command
55
+ pymdkit <command> --help # shows that command's flags
56
+ ```
57
+
58
+ ## Commands
59
+
60
+ Commands that transform structures accept either a single file (`-i`/`-o`) or a
61
+ whole folder (`-if`/`-of`); commands that analyse VASP runs scan the current
62
+ directory for job sub-folders automatically.
63
+
64
+ | Command | What it does |
65
+ |---|---|
66
+ | `add-groups` | Tag atoms with a GPUMD group index by element order |
67
+ | `ehull` | Auto-detect VASP job folders and compute E_hull vs Materials Project |
68
+ | `gather-contcar` | Collect CONTCARs from VASP job folders into one folder, renamed `<folder>.vasp` |
69
+ | `msd` | Diffusivity & conductivity for all groups from GPUMD MSD data |
70
+ | `outcar2xyz` | Collect SCF-converged VASP job folders (any name) into one extxyz file |
71
+ | `rmsd` | Compute RMSD between two structure files, or all pairs in a folder |
72
+ | `select-candidate` | Split a NEP training set into candidate/accurate sets by energy error |
73
+ | `stru2xyz` | Convert structure file(s) of any format to extxyz |
74
+ | `supercell` | Build a supercell with cell lengths capped at a maximum (Angstrom); optional per-temperature GPUMD setup |
75
+ | `symmetrize` | Import space-group symmetry into a structure file (or folder) -> CIF |
76
+ | `vasp-relax` | Write VASP relaxation inputs for a structure (or folder); INCAR tags overridable |
77
+ | `vasp-static` | Write VASP static / single-point inputs for a structure (or folder) |
78
+
79
+ ## Examples
80
+
81
+ ```bash
82
+ pymdkit add-groups -i opted.cif --elements Li Y Cl -o model.xyz
83
+ pymdkit add-groups -if cifs/ --elements Li Y Cl -of cifs-grouped/ # whole folder
84
+ pymdkit add-groups --elements Li Y Cl # scan subfolders, tag each model.xyz in place
85
+ pymdkit stru2xyz -i opted.vasp -o opted.xyz # convert one file
86
+ pymdkit stru2xyz -if vasp-opted -of xyz-opted # convert a whole folder
87
+ pymdkit stru2xyz # scan subfolders, convert structures in place
88
+ pymdkit supercell -i opted.vasp -o sc.vasp -max-abc 20 # cell lengths <= 20 A
89
+ pymdkit supercell -if vasp-opted -max-abc 20 -individual # per-structure ./<name>/<name>.<ext>
90
+ pymdkit supercell -if extxyz-opted -max-abc 24 -individual -temp 500 600 -md-if input-files
91
+ # GPUMD: ./<name>/model.xyz + ./<name>/<T>/ jobs
92
+ pymdkit vasp-relax -i opted.vasp # relax inputs in current dir
93
+ pymdkit vasp-relax -if optimal_occupancy # one ./<name>/ job folder per structure
94
+ pymdkit vasp-static -if cifs/ -custom-setting my_incar.txt # static inputs, custom INCAR
95
+ pymdkit vasp-static -it traj.xyz # one ./frame_N/ job per trajectory frame
96
+ pymdkit msd --diffuse_ion Li --ion_charge 1 --output_dir results/
97
+ pymdkit ehull --mp-api-key $MP_API_KEY # scans ./ for VASP jobs -> ehull.txt
98
+ pymdkit gather-contcar -of vasp-opted # CONTCARs -> vasp-opted/<folder>.vasp
99
+ pymdkit gather-contcar -of vasp-opted -ehull 0.028 # only structures with E_hull < 0.028 eV/atom
100
+ pymdkit outcar2xyz # scans ./ for OUTCAR folders -> scf-converged.xyz
101
+ pymdkit select-candidate # RMSE bands: <low all accurate, >high all candidate, else worst 50%
102
+ pymdkit select-candidate -r 0.8 # in the middle band, take worst 80% as candidate.xyz
103
+ pymdkit rmsd a.cif b.cif # RMSD of two files -> rmsd.txt
104
+ pymdkit rmsd vasp-opted/ # all pairs in a folder -> rmsd.txt
105
+ pymdkit symmetrize opted.cif --symprec 0.01 --add_oxidation yes -o opted-symm.cif
106
+ pymdkit symmetrize my_cifs/ --symprec 0.01 --add_oxidation no -o my_cifs-symm
107
+ ```
108
+
109
+ VASP input commands (`vasp-relax`, `vasp-static`) always produce **individual**
110
+ jobs (one structure per folder): `-i` writes into the current dir (or `-o`),
111
+ `-if` creates one `./<name>/` folder per structure, and `-it` creates one
112
+ `./frame_N/` folder per trajectory frame — all directly in the current path.
113
+
114
+ They start from sensible default INCAR settings; override them by passing a
115
+ settings file with `-custom-setting FILE`. The file may be a Python-dict block
116
+ or `KEY = VALUE` lines (a `None`/blank value clears a tag):
117
+
118
+ ```text
119
+ custom_settings = {
120
+ "ENCUT": "600.0",
121
+ "ISIF": "3",
122
+ "MAGMOM": None
123
+ }
124
+ ```
125
+
126
+ `vasp-static -it traj.xyz` (also available on `vasp-relax`) reads a
127
+ multi-structure trajectory and writes one job sub-folder per frame
128
+ (`frame_1/`, `frame_2/`, …, prefix configurable via `--frame-prefix`). Each
129
+ folder also keeps a `frame_N.xyz`, so `Config_type` survives for a later
130
+ `outcar2xyz`.
131
+
132
+ Each command's full flag list is in `pymdkit <command> --help`.
133
+
134
+ `ehull` auto-detects every sub-folder of the current path that contains a
135
+ `vasprun.xml`, groups them by chemical system (elements ordered by
136
+ electronegativity, e.g. `Li-Y-Cl`), and builds/reuses one `mp_cache_<system>.json`
137
+ per system — so a pure Li-Y-Cl batch yields a single `mp_cache_Li-Y-Cl.json`, while
138
+ a mixed Li-Y-Cl + La-O batch yields both `mp_cache_Li-Y-Cl.json` and
139
+ `mp_cache_La-O.json`. (Formation energy is reported alongside E_hull in
140
+ `ehull.txt`.)
141
+
142
+ ## Layout
143
+
144
+ ```
145
+ pymdkit/
146
+ ├── pyproject.toml # package metadata + the `pymdkit` entry point
147
+ ├── README.md
148
+ └── src/pymdkit/
149
+ ├── pymdkit_main.py # dispatcher: discovers and runs commands
150
+ └── commands/ # one module per command
151
+ ├── _fileio.py # shared -i/-o/-if/-of helper (not a command)
152
+ ├── _vaspset.py # shared VASP input-set helper (not a command)
153
+ ├── add_groups.py
154
+ ├── compute_ehull.py
155
+ ├── compute_rmsd.py
156
+ ├── outcar2xyz.py
157
+ ├── stru2xyz.py
158
+ ├── supercell.py
159
+ ├── vasp_relax.py
160
+ ├── vasp_static.py
161
+ ├── ...
162
+ └── symmetrize.py
163
+ ```
164
+
165
+ Modules whose name starts with `_` are shared helpers and are skipped by the
166
+ dispatcher, so they never appear as commands.
167
+
168
+ ## Adding a new tool later
169
+
170
+ Drop a module in `src/pymdkit/commands/` that defines four things:
171
+
172
+ ```python
173
+ COMMAND = "my-tool" # the subcommand name you'll type
174
+ HELP = "One-line description."
175
+
176
+ def add_arguments(parser): # register flags
177
+ parser.add_argument("--input", required=True)
178
+
179
+ def run(args): # do the work; return an exit code (0 = ok)
180
+ ...
181
+ return 0
182
+
183
+ if __name__ == "__main__": # keeps the script runnable on its own
184
+ import argparse
185
+ _p = argparse.ArgumentParser(description=__doc__)
186
+ add_arguments(_p)
187
+ raise SystemExit(run(_p.parse_args()))
188
+ ```
189
+
190
+ It will appear in `pymdkit --help` automatically — no central registration needed.
191
+ Put heavy imports (pymatgen, ase, …) inside `run()` where practical; the dispatcher
192
+ reads each command's name and help without importing it, so `pymdkit --help` stays
193
+ fast and a missing optional dependency only affects the one command that needs it.
194
+
195
+ ## Running a script standalone
196
+
197
+ Every command module still works directly, which is handy for debugging:
198
+
199
+ ```bash
200
+ python src/pymdkit/commands/supercell.py -i in.cif -max-abc 20 -o sc.vasp
201
+ ```
@@ -0,0 +1,23 @@
1
+ pymdkit/__init__.py,sha256=sHDqx87K1gtY_sHTD1ZMsNh7DtsjU4HKu1C79CHtVCg,94
2
+ pymdkit/pymdkit_main.py,sha256=IcfM0c5pMVM2vDcHUlPMJlatTKzA-k5jaH6oXr8rKVA,4160
3
+ pymdkit/commands/__init__.py,sha256=nN8YQnb7udqPmNxIs7AeIp2qjZdh0JC2Q4wpuvZ4pv0,84
4
+ pymdkit/commands/_fileio.py,sha256=xs5VVFSqJof74ojoDwITYc87j-Rs1USIP-kRyrK7YM4,3696
5
+ pymdkit/commands/_vaspset.py,sha256=ky8LHY6S7vBIci4gv1hL7ezoKKK3r7YXcdpuklmalR4,7100
6
+ pymdkit/commands/add_groups.py,sha256=6iZljChDqghYc8yW3S6hjknJ4TbsZDFKTJznY4fq6fA,2536
7
+ pymdkit/commands/compute_ehull.py,sha256=E5RpsVDejGfq_iY3gtjt9HZsx9A0Ykch0CrAAIhXn_c,8486
8
+ pymdkit/commands/compute_msd_all_groups.py,sha256=SGKcdD9_N5f4yxo1_KDN8upePELF9HKlJySiw1gJNro,9599
9
+ pymdkit/commands/compute_rmsd.py,sha256=Zv8lSvWnuvq7Tse92WpM8gMu1cOW8mXZlIJ7ptb4gWE,4626
10
+ pymdkit/commands/gather_contcar.py,sha256=ULszT5DIO30DoROthXgsfBeGpapx7IggnY1ESTFF0xA,3657
11
+ pymdkit/commands/outcar2xyz.py,sha256=43l74FyezhAkrxtrUfEDaxoQU5ZSbhy6ltJrhc9QeLk,4753
12
+ pymdkit/commands/select_candidate.py,sha256=7OB0fls_XJG5EPbSf4Z4xaY9dp83TAlCAufy0t7YVP0,4862
13
+ pymdkit/commands/stru2xyz.py,sha256=bb225psD9AGtVrxif6KsUax0k1_GjaIazHvhT6OvqRU,3441
14
+ pymdkit/commands/supercell.py,sha256=1ca6ZmO9e2F_FAj_GMpAwEOmttludEQwzu6T05exg-k,6583
15
+ pymdkit/commands/symmetrize.py,sha256=z7b0SvvcWyun0i4D6zeBI6_B1fYhRI1x6MaIxKQiey8,10580
16
+ pymdkit/commands/vasp_relax.py,sha256=s_DTFMSwr1eL9sPb-nLWUPY6nsyCts-aMi4tDVJquYs,1599
17
+ pymdkit/commands/vasp_static.py,sha256=Y2EHNTM7kkfa-K-zF1f2DC-rsf-LPwPlkvX40K07c0E,1605
18
+ pymdkit-1.0.0.dist-info/licenses/LICENSE,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
19
+ pymdkit-1.0.0.dist-info/METADATA,sha256=FPHZBegHo0udyw-3c6E2f4RMrGjxjR08-ZZtwgLu2YE,9278
20
+ pymdkit-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
21
+ pymdkit-1.0.0.dist-info/entry_points.txt,sha256=20H-GHgwhttbzjAgRVk_KLlYfhNtk7BMD9ho6mTFDZ8,54
22
+ pymdkit-1.0.0.dist-info/top_level.txt,sha256=S215lTymVM5DZmDmG_bg9pMrtQ3z0ruY9LNL3Ud-inI,8
23
+ pymdkit-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pymdkit = pymdkit.pymdkit_main:main