phistory 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.
- phistory/__init__.py +40 -0
- phistory/__version__.py +1 -0
- phistory/core.py +225 -0
- phistory/yaml_args.py +156 -0
- phistory-0.2.0.dist-info/METADATA +130 -0
- phistory-0.2.0.dist-info/RECORD +9 -0
- phistory-0.2.0.dist-info/WHEEL +5 -0
- phistory-0.2.0.dist-info/licenses/LICENSE +21 -0
- phistory-0.2.0.dist-info/top_level.txt +1 -0
phistory/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
phistory: seamless argparse execution history.
|
|
3
|
+
|
|
4
|
+
Import this at the very top of your script:
|
|
5
|
+
|
|
6
|
+
import phistory
|
|
7
|
+
|
|
8
|
+
Behaviour:
|
|
9
|
+
- On import, if '--history' is present in sys.argv, prints history for the current
|
|
10
|
+
script and exits.
|
|
11
|
+
- Patches argparse's parse_args/parse_known_args so that, on first call, the
|
|
12
|
+
invocation (script name + args) is appended to ~/.python-history/<script>.history
|
|
13
|
+
(excluding the --history flag itself).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .core import install as _install
|
|
17
|
+
import os
|
|
18
|
+
|
|
19
|
+
# Allow disabling automatic install during import for tests or introspection.
|
|
20
|
+
# Set the environment variable PHISTORY_NO_AUTO_INSTALL=1 to suppress auto-hooks.
|
|
21
|
+
if os.environ.get("PHISTORY_NO_AUTO_INSTALL") != "1":
|
|
22
|
+
_install()
|
|
23
|
+
|
|
24
|
+
# phistory/__init__.py (append to the bottom)
|
|
25
|
+
from .yaml_args import (
|
|
26
|
+
load as yaml_args_load,
|
|
27
|
+
derive_params_filename,
|
|
28
|
+
load_yaml_params,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# nice namespace: phistory.yaml_args.load
|
|
32
|
+
from . import yaml_args # re-export module for ergonomic `from phistory import yaml_args`
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
# existing…
|
|
36
|
+
"yaml_args",
|
|
37
|
+
"yaml_args_load",
|
|
38
|
+
"derive_params_filename",
|
|
39
|
+
"load_yaml_params",
|
|
40
|
+
]
|
phistory/__version__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
phistory/core.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import shlex
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Iterable, Optional, Tuple
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
# Guard to avoid writing multiple times per process execution.
|
|
11
|
+
_HISTORY_WRITTEN = False
|
|
12
|
+
|
|
13
|
+
# Valid suboptions that may follow --history
|
|
14
|
+
_HISTORY_SUBOPTS = {"date", "unique"}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _history_dir() -> Path:
|
|
18
|
+
"""
|
|
19
|
+
Compute the history directory dynamically each time so changes to HOME
|
|
20
|
+
are respected. On Windows, prefer LOCALAPPDATA (then APPDATA).
|
|
21
|
+
- Windows: %LOCALAPPDATA%/Python/phistory/history
|
|
22
|
+
- Others : ~/.python-history
|
|
23
|
+
"""
|
|
24
|
+
if os.name == "nt":
|
|
25
|
+
base = os.getenv("LOCALAPPDATA") or os.getenv("APPDATA")
|
|
26
|
+
if base:
|
|
27
|
+
return Path(base) / "Python" / "phistory" / "history"
|
|
28
|
+
# Fallback if env vars are missing
|
|
29
|
+
return Path.home() / "AppData" / "Local" / "Python" / "phistory" / "history"
|
|
30
|
+
# POSIX default
|
|
31
|
+
return Path.home() / ".python-history"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _script_name() -> str:
|
|
35
|
+
# Fall back to "script" if argv[0] is empty.
|
|
36
|
+
return os.path.basename(sys.argv[0]) or "script"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _history_file_for(script_name: Optional[str] = None) -> Path:
|
|
40
|
+
if script_name is None:
|
|
41
|
+
script_name = _script_name()
|
|
42
|
+
return _history_dir() / f"{script_name}.history"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _ensure_dir():
|
|
46
|
+
_history_dir().mkdir(parents=True, exist_ok=True)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _parse_history_options(argv: Iterable[str]) -> Tuple[bool, bool]:
|
|
50
|
+
"""
|
|
51
|
+
Scan argv for --history and its optional suboptions.
|
|
52
|
+
Returns: (want_date, want_unique).
|
|
53
|
+
"""
|
|
54
|
+
want_date = False
|
|
55
|
+
want_unique = False
|
|
56
|
+
tokens = list(argv)
|
|
57
|
+
for i, tok in enumerate(tokens):
|
|
58
|
+
if tok == "--history":
|
|
59
|
+
# Consume following suboptions while valid (order agnostic)
|
|
60
|
+
j = i + 1
|
|
61
|
+
while j < len(tokens) and tokens[j] in _HISTORY_SUBOPTS:
|
|
62
|
+
if tokens[j] == "date":
|
|
63
|
+
want_date = True
|
|
64
|
+
elif tokens[j] == "unique":
|
|
65
|
+
want_unique = True
|
|
66
|
+
j += 1
|
|
67
|
+
break
|
|
68
|
+
return want_date, want_unique
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _filter_args(argv: Iterable[str]) -> list[str]:
|
|
72
|
+
"""
|
|
73
|
+
Remove --history and its recognized suboptions from the provided argv.
|
|
74
|
+
Leave everything else intact.
|
|
75
|
+
"""
|
|
76
|
+
filtered = []
|
|
77
|
+
it = iter(list(argv))
|
|
78
|
+
for a in it:
|
|
79
|
+
if a == "--history":
|
|
80
|
+
# Skip any recognized suboptions that immediately follow (max two)
|
|
81
|
+
consumed = 0
|
|
82
|
+
while consumed < 2:
|
|
83
|
+
try:
|
|
84
|
+
nxt = next(it)
|
|
85
|
+
except StopIteration:
|
|
86
|
+
break
|
|
87
|
+
if nxt in _HISTORY_SUBOPTS:
|
|
88
|
+
consumed += 1
|
|
89
|
+
continue
|
|
90
|
+
# Not a recognized suboption: push back into filtered
|
|
91
|
+
filtered.append(nxt)
|
|
92
|
+
break
|
|
93
|
+
continue
|
|
94
|
+
filtered.append(a)
|
|
95
|
+
return filtered
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _join_args_for_shell(args: list[str]) -> str:
|
|
99
|
+
"""
|
|
100
|
+
Platform-appropriate quoting for replayable commands.
|
|
101
|
+
- Windows: use subprocess.list2cmdline (CreateProcess/CommandLineToArgvW rules)
|
|
102
|
+
- POSIX : use shlex.quote
|
|
103
|
+
"""
|
|
104
|
+
if os.name == "nt":
|
|
105
|
+
return subprocess.list2cmdline(args)
|
|
106
|
+
return " ".join(shlex.quote(a) for a in args)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _record_invocation(argv: Optional[Iterable[str]] = None):
|
|
110
|
+
"""
|
|
111
|
+
Append a single line to the history file.
|
|
112
|
+
Stored format: "<ISO_DATETIME>\t<commandline>"
|
|
113
|
+
ISO datetime uses seconds precision in local time.
|
|
114
|
+
"""
|
|
115
|
+
global _HISTORY_WRITTEN
|
|
116
|
+
if _HISTORY_WRITTEN:
|
|
117
|
+
return
|
|
118
|
+
if argv is None:
|
|
119
|
+
argv = sys.argv[1:]
|
|
120
|
+
|
|
121
|
+
args_list = list(argv)
|
|
122
|
+
args_list = _filter_args(args_list)
|
|
123
|
+
|
|
124
|
+
# Compose a fully copy/paste-able command line: "<script> <args...>"
|
|
125
|
+
command = _script_name()
|
|
126
|
+
if args_list:
|
|
127
|
+
command += " " + _join_args_for_shell(args_list)
|
|
128
|
+
|
|
129
|
+
_ensure_dir()
|
|
130
|
+
hf = _history_file_for()
|
|
131
|
+
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
132
|
+
with hf.open("a", encoding="utf-8") as f:
|
|
133
|
+
f.write(f"{ts}\t{command}\n")
|
|
134
|
+
|
|
135
|
+
_HISTORY_WRITTEN = True
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _render_history_line(raw_line: str, want_date: bool) -> Optional[str]:
|
|
139
|
+
"""
|
|
140
|
+
Render one history line for output depending on want_date.
|
|
141
|
+
History lines may be either:
|
|
142
|
+
1) "<ts>\t<command>" (new format), or
|
|
143
|
+
2) "<command>" (legacy format)
|
|
144
|
+
Returns the printable string, or None if the line is empty.
|
|
145
|
+
"""
|
|
146
|
+
line = raw_line.rstrip("\n")
|
|
147
|
+
if not line:
|
|
148
|
+
return None
|
|
149
|
+
if "\t" in line:
|
|
150
|
+
ts, cmd = line.split("\t", 1)
|
|
151
|
+
return f"{ts} {cmd}" if want_date else cmd
|
|
152
|
+
# legacy format (no timestamp)
|
|
153
|
+
return line if not want_date else line # no ts to show; print command only
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _print_history_and_exit():
|
|
157
|
+
want_date, want_unique = _parse_history_options(sys.argv[1:])
|
|
158
|
+
hf = _history_file_for()
|
|
159
|
+
seen = set()
|
|
160
|
+
output = []
|
|
161
|
+
|
|
162
|
+
if hf.exists():
|
|
163
|
+
with hf.open("r", encoding="utf-8") as f:
|
|
164
|
+
for raw in f:
|
|
165
|
+
# For uniqueness, dedupe by the *command* part (strip timestamp if present)
|
|
166
|
+
dedupe_key = raw.split("\t", 1)[-1].rstrip("\n")
|
|
167
|
+
if want_unique:
|
|
168
|
+
if dedupe_key in seen:
|
|
169
|
+
continue
|
|
170
|
+
seen.add(dedupe_key)
|
|
171
|
+
rendered = _render_history_line(raw, want_date=want_date)
|
|
172
|
+
if rendered is not None:
|
|
173
|
+
output.append(rendered)
|
|
174
|
+
|
|
175
|
+
if output:
|
|
176
|
+
sys.stdout.write("\n".join(output) + "\n")
|
|
177
|
+
else:
|
|
178
|
+
# No history yet; keep output friendly but still copyable (empty)
|
|
179
|
+
sys.stdout.write("")
|
|
180
|
+
raise SystemExit(0)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _check_and_handle_history_flag():
|
|
184
|
+
if "--history" in sys.argv[1:]:
|
|
185
|
+
_print_history_and_exit()
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _patch_argparse():
|
|
189
|
+
"""Monkey-patch argparse to record once per process when parse_* is called."""
|
|
190
|
+
# Only patch once
|
|
191
|
+
if getattr(argparse.ArgumentParser, "_phistory_patched", False):
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
_orig_parse_args = argparse.ArgumentParser.parse_args
|
|
195
|
+
_orig_parse_known_args = argparse.ArgumentParser.parse_known_args
|
|
196
|
+
|
|
197
|
+
def _wrap_parse_args(self, args=None, namespace=None):
|
|
198
|
+
# Normalize incoming args (explicit or sys.argv) and strip --history so argparse won't error.
|
|
199
|
+
incoming = list(args) if args is not None else list(sys.argv[1:])
|
|
200
|
+
filtered = _filter_args(incoming)
|
|
201
|
+
# If args is None, pass None so argparse reads from sys.argv; we still record using filtered.
|
|
202
|
+
result = _orig_parse_args(self, args=filtered if args is not None else None, namespace=namespace)
|
|
203
|
+
_record_invocation(filtered)
|
|
204
|
+
return result
|
|
205
|
+
|
|
206
|
+
def _wrap_parse_known_args(self, args=None, namespace=None):
|
|
207
|
+
incoming = list(args) if args is not None else list(sys.argv[1:])
|
|
208
|
+
filtered = _filter_args(incoming)
|
|
209
|
+
result = _orig_parse_known_args(self, args=filtered if args is not None else None, namespace=namespace)
|
|
210
|
+
_record_invocation(filtered)
|
|
211
|
+
return result
|
|
212
|
+
|
|
213
|
+
argparse.ArgumentParser.parse_args = _wrap_parse_args
|
|
214
|
+
argparse.ArgumentParser.parse_known_args = _wrap_parse_known_args
|
|
215
|
+
argparse.ArgumentParser._phistory_patched = True # sentinel
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def install():
|
|
219
|
+
"""
|
|
220
|
+
Entry point called by package import.
|
|
221
|
+
- If '--history' present, print history (with options) and exit.
|
|
222
|
+
- Otherwise patch argparse to record on first parse.
|
|
223
|
+
"""
|
|
224
|
+
_check_and_handle_history_flag()
|
|
225
|
+
_patch_argparse()
|
phistory/yaml_args.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""
|
|
2
|
+
phistory.yaml_args
|
|
3
|
+
Ultra-minimal YAML-based argument loader to complement phistory's zero-config history.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
from phistory import yaml_args
|
|
7
|
+
args = yaml_args.load() # looks in PWD for "<script>.params.yaml"
|
|
8
|
+
# or: args = yaml_args.load("/path/to/params.yaml")
|
|
9
|
+
|
|
10
|
+
Conventions:
|
|
11
|
+
- Default params filename is derived from your script name and searched in the
|
|
12
|
+
current working directory (pwd): "<script_stem>.params.yaml".
|
|
13
|
+
- If the user supplies a single positional argument (that does not start with "-"),
|
|
14
|
+
it is treated as the params file path (absolute or relative).
|
|
15
|
+
- You can also pass params_file explicitly.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
- argparse.Namespace by default (attribute access), or a dict with as_namespace=False.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
import os
|
|
23
|
+
import re
|
|
24
|
+
import sys
|
|
25
|
+
from argparse import Namespace
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any, Dict, Iterable, Mapping, Optional
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
import yaml # type: ignore
|
|
31
|
+
except Exception as e: # pragma: no cover
|
|
32
|
+
raise RuntimeError(
|
|
33
|
+
"PyYAML is required for phistory.yaml_args. "
|
|
34
|
+
"Install with: pip install PyYAML"
|
|
35
|
+
) from e
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def derive_params_filename(script_path: Optional[str] = None, suffix: str = ".params.yaml") -> str:
|
|
39
|
+
"""
|
|
40
|
+
Derive "<script_stem>.params.yaml" from a script path.
|
|
41
|
+
Example: "/x/run_experiment.py" -> "run_experiment.params.yaml"
|
|
42
|
+
"""
|
|
43
|
+
if not script_path:
|
|
44
|
+
script_path = sys.argv[0] or "script.py"
|
|
45
|
+
stem = re.sub(r"\.py$", "", os.path.basename(script_path))
|
|
46
|
+
return f"{stem}{suffix}"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def load_yaml_params(params_filename: str) -> Dict[str, Any]:
|
|
50
|
+
"""
|
|
51
|
+
Load YAML from disk.
|
|
52
|
+
- Raises FileNotFoundError if the file does not exist.
|
|
53
|
+
- Returns {} if the YAML is empty.
|
|
54
|
+
"""
|
|
55
|
+
path = Path(params_filename)
|
|
56
|
+
if not path.exists():
|
|
57
|
+
raise FileNotFoundError(f"Parameters file not found: {params_filename}")
|
|
58
|
+
with path.open("r", encoding="utf-8") as f:
|
|
59
|
+
return yaml.safe_load(f) or {}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def require_keys(params: Mapping[str, Any], required: Iterable[str]) -> None:
|
|
63
|
+
"""Raise ValueError if any required key is missing."""
|
|
64
|
+
missing = [k for k in required if k not in params]
|
|
65
|
+
if missing:
|
|
66
|
+
raise ValueError(f"Missing required parameter(s) in YAML: {', '.join(missing)}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _positional_argv_candidate() -> Optional[str]:
|
|
70
|
+
"""
|
|
71
|
+
Return the first positional (non-flag) argument from sys.argv (if any).
|
|
72
|
+
Flags start with '-'. Only checks argv[1], in keeping with the "single argument" UX.
|
|
73
|
+
"""
|
|
74
|
+
if len(sys.argv) >= 2:
|
|
75
|
+
first = sys.argv[1]
|
|
76
|
+
if first and not first.startswith("-"):
|
|
77
|
+
return first
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def load(
|
|
82
|
+
params_file: Optional[str] = None,
|
|
83
|
+
required: Optional[Iterable[str]] = None,
|
|
84
|
+
*,
|
|
85
|
+
as_namespace: bool = True,
|
|
86
|
+
) -> Namespace | Dict[str, Any]:
|
|
87
|
+
"""
|
|
88
|
+
Load parameters from YAML with zero script-side config.
|
|
89
|
+
|
|
90
|
+
Resolution order:
|
|
91
|
+
1) If params_file is provided, use it.
|
|
92
|
+
2) Else if a single positional (non-flag) argv is present and points to an existing file, use it.
|
|
93
|
+
3) Else look in the current working directory for "<script_stem>.params.yaml".
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
params_file: optional path to YAML.
|
|
97
|
+
required: optional iterable of required keys to enforce.
|
|
98
|
+
as_namespace: if True (default) return argparse.Namespace; else a dict.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
argparse.Namespace (default) or dict with the YAML keys as attributes/keys.
|
|
102
|
+
"""
|
|
103
|
+
searched: list[str] = []
|
|
104
|
+
|
|
105
|
+
# 1) Explicit path wins
|
|
106
|
+
if params_file:
|
|
107
|
+
pf_path = Path(params_file)
|
|
108
|
+
searched.append(str(pf_path))
|
|
109
|
+
if not pf_path.exists():
|
|
110
|
+
raise FileNotFoundError(
|
|
111
|
+
f"Parameters file not found: {pf_path}"
|
|
112
|
+
)
|
|
113
|
+
params = load_yaml_params(str(pf_path))
|
|
114
|
+
else:
|
|
115
|
+
# 2) Single positional argv candidate
|
|
116
|
+
cand = _positional_argv_candidate()
|
|
117
|
+
if cand:
|
|
118
|
+
cand_path = Path(cand)
|
|
119
|
+
searched.append(str(cand_path))
|
|
120
|
+
if cand_path.exists():
|
|
121
|
+
params = load_yaml_params(str(cand_path))
|
|
122
|
+
else:
|
|
123
|
+
# 3) Fall back to default in PWD
|
|
124
|
+
default_name = derive_params_filename(sys.argv[0])
|
|
125
|
+
default_path = Path.cwd() / default_name
|
|
126
|
+
searched.append(str(default_path))
|
|
127
|
+
if not default_path.exists():
|
|
128
|
+
raise FileNotFoundError(
|
|
129
|
+
"Could not locate a parameters YAML.\n"
|
|
130
|
+
f"Tried:\n - {cand_path}\n - {default_path}\n\n"
|
|
131
|
+
"Tips:\n"
|
|
132
|
+
" • Put '<script_stem>.params.yaml' in the current working directory, or\n"
|
|
133
|
+
" • Provide a single positional argument with the path to the YAML, or\n"
|
|
134
|
+
" • Call yaml_args.load(params_file='/path/to/file.yaml')."
|
|
135
|
+
)
|
|
136
|
+
params = load_yaml_params(str(default_path))
|
|
137
|
+
else:
|
|
138
|
+
# 3) Default in PWD
|
|
139
|
+
default_name = derive_params_filename(sys.argv[0])
|
|
140
|
+
default_path = Path.cwd() / default_name
|
|
141
|
+
searched.append(str(default_path))
|
|
142
|
+
if not default_path.exists():
|
|
143
|
+
raise FileNotFoundError(
|
|
144
|
+
f"Parameters file not found in current directory: {default_path}\n"
|
|
145
|
+
"Tips:\n"
|
|
146
|
+
" • Put '<script_stem>.params.yaml' in the current working directory, or\n"
|
|
147
|
+
" • Provide a single positional argument with the path to the YAML, or\n"
|
|
148
|
+
" • Call yaml_args.load(params_file='/path/to/file.yaml')."
|
|
149
|
+
)
|
|
150
|
+
params = load_yaml_params(str(default_path))
|
|
151
|
+
|
|
152
|
+
if required:
|
|
153
|
+
require_keys(params, required)
|
|
154
|
+
|
|
155
|
+
return Namespace(**params) if as_namespace else params
|
|
156
|
+
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: phistory
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Zero-config argparse history recorder and replayer
|
|
5
|
+
Author: amorriso
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/amorriso/phistory
|
|
8
|
+
Project-URL: Source, https://github.com/amorriso/phistory
|
|
9
|
+
Project-URL: Issues, https://github.com/amorriso/phistory/issues
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Intended Audience :: Developers
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: PyYAML>=6.0
|
|
24
|
+
Dynamic: author
|
|
25
|
+
Dynamic: classifier
|
|
26
|
+
Dynamic: description
|
|
27
|
+
Dynamic: description-content-type
|
|
28
|
+
Dynamic: license
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
Dynamic: project-url
|
|
31
|
+
Dynamic: requires-dist
|
|
32
|
+
Dynamic: requires-python
|
|
33
|
+
Dynamic: summary
|
|
34
|
+
|
|
35
|
+
# phistory
|
|
36
|
+
|
|
37
|
+
`phistory` makes Python scripts remember how they were run, and provides a
|
|
38
|
+
small YAML parameter loader for scripts that do not use `argparse`.
|
|
39
|
+
|
|
40
|
+
It supports Python 3.10+.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Quickstart
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
# myscript.py
|
|
48
|
+
import phistory # must be first
|
|
49
|
+
import argparse
|
|
50
|
+
|
|
51
|
+
p = argparse.ArgumentParser()
|
|
52
|
+
p.add_argument("--foo")
|
|
53
|
+
p.add_argument("bar")
|
|
54
|
+
args = p.parse_args()
|
|
55
|
+
print(args)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Run
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
python myscript.py --foo 123 hello
|
|
62
|
+
python myscript.py --foo 999 world
|
|
63
|
+
python myscript.py --history
|
|
64
|
+
# outputs:
|
|
65
|
+
# myscript.py --foo 123 hello
|
|
66
|
+
# myscript.py --foo 999 world
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## What it does
|
|
72
|
+
|
|
73
|
+
- Saves each execution’s CLI (`script name + args`) to `~/.python-history/<script>.history`.
|
|
74
|
+
- When run with `--history`, prints previous runs (copy/paste friendly) and exits.
|
|
75
|
+
- Only writes history when your script calls `argparse.parse_args` or `parse_known_args`.
|
|
76
|
+
- The `--history` flag itself is never recorded.
|
|
77
|
+
|
|
78
|
+
**History directory:** `~/.python-history/`
|
|
79
|
+
|
|
80
|
+
Use `--history date` to include timestamps, `--history unique` to show only
|
|
81
|
+
the first occurrence of each command, or both options together.
|
|
82
|
+
|
|
83
|
+
> `phistory` intentionally monkey-patches `argparse.ArgumentParser` when it is
|
|
84
|
+
> imported. Import it before importing or configuring `argparse` in a script
|
|
85
|
+
> that should record history.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## YAML args (no argparse required)
|
|
90
|
+
|
|
91
|
+
Skip `argparse` entirely and just load parameters from YAML.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from phistory import yaml_args
|
|
95
|
+
|
|
96
|
+
args = yaml_args.load() # looks for "<script>.params.yaml" in the current directory
|
|
97
|
+
print(args.outpath) # access fields as attributes
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Behavior
|
|
101
|
+
|
|
102
|
+
- Default file: `<script_stem>.params.yaml` (e.g. `runner.params.yaml`)
|
|
103
|
+
- You can also provide the path explicitly or as a single argument:
|
|
104
|
+
```bash
|
|
105
|
+
python runner.py configs/myexp.yaml
|
|
106
|
+
```
|
|
107
|
+
- Validate required keys:
|
|
108
|
+
```python
|
|
109
|
+
args = yaml_args.load(required=["experiment-name", "outpath", "description"])
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Helper functions
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from phistory import derive_params_filename, load_yaml_params
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Installation
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
pip install phistory
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
phistory/__init__.py,sha256=I5uxXrQpzOMRLqU_bJX0t1vkPPVFGJe2LRMnhD2xT0s,1130
|
|
2
|
+
phistory/__version__.py,sha256=Zn1KFblwuFHiDRdRAiRnDBRkbPttWh44jKa5zG2ov0E,22
|
|
3
|
+
phistory/core.py,sha256=7hmdhEW3R8A9W1GNsHARRNh6SkwMkCQT4Ke-sWpDzR8,7451
|
|
4
|
+
phistory/yaml_args.py,sha256=TxtwW1WcCuL4IRvU6AGLPFqDWnHf93adTSEPH8g3Sjs,5836
|
|
5
|
+
phistory-0.2.0.dist-info/licenses/LICENSE,sha256=xmHWdB9nNzwGSfRRXQYlj7Bpr-6e88v2BFgnBWww7gI,1060
|
|
6
|
+
phistory-0.2.0.dist-info/METADATA,sha256=Jkg8VlcFUyCfO7ZZ1MYBSXNyvqrQ9tF-71sjVrlnO2I,3274
|
|
7
|
+
phistory-0.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
8
|
+
phistory-0.2.0.dist-info/top_level.txt,sha256=u0os2XEPdcZq-jXjJBBlDerXYzff-6AUjHQeihmxmYk,9
|
|
9
|
+
phistory-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tmo
|
|
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
|
+
phistory
|