calkit-python 0.48.0__py3-none-any.whl → 0.48.1__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.
- calkit/cli/latex.py +33 -1
- calkit/cli/update.py +36 -13
- calkit/core.py +94 -0
- calkit/docx.py +68 -9
- calkit/dvc/core.py +12 -2
- calkit/fs.py +7 -3
- calkit/hub.py +8 -1
- calkit/models/pipeline.py +22 -2
- calkit/tests/cli/test_notebooks.py +6 -0
- calkit/tests/cli/test_update.py +51 -5
- calkit/tests/dvc/test_core.py +45 -0
- calkit/tests/models/test_pipeline.py +59 -0
- calkit/tests/test_core.py +50 -0
- calkit/tests/test_docx.py +53 -0
- calkit/tests/test_hub.py +8 -8
- {calkit_python-0.48.0.dist-info → calkit_python-0.48.1.dist-info}/METADATA +1 -1
- {calkit_python-0.48.0.dist-info → calkit_python-0.48.1.dist-info}/RECORD +33 -33
- {calkit_python-0.48.0.data → calkit_python-0.48.1.data}/data/etc/jupyter/jupyter_server_config.d/calkit.json +0 -0
- {calkit_python-0.48.0.data → calkit_python-0.48.1.data}/data/share/jupyter/labextensions/calkit/package.json +0 -0
- {calkit_python-0.48.0.data → calkit_python-0.48.1.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/package.json.orig +0 -0
- {calkit_python-0.48.0.data → calkit_python-0.48.1.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/plugin.json +0 -0
- {calkit_python-0.48.0.data → calkit_python-0.48.1.data}/data/share/jupyter/labextensions/calkit/static/506.c34b070098184e33.js +0 -0
- {calkit_python-0.48.0.data → calkit_python-0.48.1.data}/data/share/jupyter/labextensions/calkit/static/57d13a7399cec3c5.png +0 -0
- {calkit_python-0.48.0.data → calkit_python-0.48.1.data}/data/share/jupyter/labextensions/calkit/static/616.528427eb54a4a0c0.js +0 -0
- {calkit_python-0.48.0.data → calkit_python-0.48.1.data}/data/share/jupyter/labextensions/calkit/static/740.6bf87276e9e5788a.js +0 -0
- {calkit_python-0.48.0.data → calkit_python-0.48.1.data}/data/share/jupyter/labextensions/calkit/static/899.f9b9f9bf705a6493.js +0 -0
- {calkit_python-0.48.0.data → calkit_python-0.48.1.data}/data/share/jupyter/labextensions/calkit/static/935.46ecc6bf99aa593a.js +0 -0
- {calkit_python-0.48.0.data → calkit_python-0.48.1.data}/data/share/jupyter/labextensions/calkit/static/remoteEntry.109a3a8379365e59.js +0 -0
- {calkit_python-0.48.0.data → calkit_python-0.48.1.data}/data/share/jupyter/labextensions/calkit/static/style.js +0 -0
- {calkit_python-0.48.0.data → calkit_python-0.48.1.data}/data/share/jupyter/labextensions/calkit/static/third-party-licenses.json +0 -0
- {calkit_python-0.48.0.dist-info → calkit_python-0.48.1.dist-info}/WHEEL +0 -0
- {calkit_python-0.48.0.dist-info → calkit_python-0.48.1.dist-info}/entry_points.txt +0 -0
- {calkit_python-0.48.0.dist-info → calkit_python-0.48.1.dist-info}/licenses/LICENSE +0 -0
calkit/cli/latex.py
CHANGED
|
@@ -2121,6 +2121,15 @@ def to_docx(
|
|
|
2121
2121
|
)
|
|
2122
2122
|
maths = calkit.docx.latex_to_omml([p[2] for p in pieces], preamble)
|
|
2123
2123
|
converted = dict(zip([(id(p[0]), p[1]) for p in pieces], maths))
|
|
2124
|
+
# What each link within the document shows, e.g., an equation's
|
|
2125
|
+
# number, by the bookmark it goes to
|
|
2126
|
+
link_text = {
|
|
2127
|
+
h.get(calkit.docx._tag(calkit.docx.W, "anchor"), ""): "".join(
|
|
2128
|
+
t.text or ""
|
|
2129
|
+
for t in h.iter(calkit.docx._tag(calkit.docx.W, "t"))
|
|
2130
|
+
).strip()
|
|
2131
|
+
for h in doc.doc.iter(calkit.docx._tag(calkit.docx.W, "hyperlink"))
|
|
2132
|
+
}
|
|
2124
2133
|
bid = 100000
|
|
2125
2134
|
last_number: str | None = None
|
|
2126
2135
|
for group, gap, numbers, lead, before_para, after_para in plans:
|
|
@@ -2156,7 +2165,30 @@ def to_docx(
|
|
|
2156
2165
|
):
|
|
2157
2166
|
if para.element is not None and para.element in unit.iter(w_p):
|
|
2158
2167
|
doc.move_out(para.element, unit, after=end)
|
|
2159
|
-
|
|
2168
|
+
# The conversion's own bookmarks on the equations, which
|
|
2169
|
+
# references in the text link to, wholly inside what's replaced,
|
|
2170
|
+
# each on the row whose number the references show
|
|
2171
|
+
w_name = calkit.docx._tag(calkit.docx.W, "name")
|
|
2172
|
+
w_id = calkit.docx._tag(calkit.docx.W, "id")
|
|
2173
|
+
ended = {
|
|
2174
|
+
e.get(w_id)
|
|
2175
|
+
for el in gap
|
|
2176
|
+
for e in el.iter(
|
|
2177
|
+
calkit.docx._tag(calkit.docx.W, "bookmarkEnd")
|
|
2178
|
+
)
|
|
2179
|
+
}
|
|
2180
|
+
row_of = {num: i for i, (_, num, _) in enumerate(rows) if num}
|
|
2181
|
+
keep: dict[int, list[tuple[str, str]]] = {}
|
|
2182
|
+
for el in gap:
|
|
2183
|
+
for b in el.iter(
|
|
2184
|
+
calkit.docx._tag(calkit.docx.W, "bookmarkStart")
|
|
2185
|
+
):
|
|
2186
|
+
name = b.get(w_name, "")
|
|
2187
|
+
if name in original or b.get(w_id) not in ended:
|
|
2188
|
+
continue
|
|
2189
|
+
row = row_of.get(link_text.get(name, ""), 0)
|
|
2190
|
+
keep.setdefault(row, []).append((name, b.get(w_id, "")))
|
|
2191
|
+
doc.insert_equations(gap[0], rows, bid, keep=keep)
|
|
2160
2192
|
bid += len(rows)
|
|
2161
2193
|
for el in gap:
|
|
2162
2194
|
doc.remove(el)
|
calkit/cli/update.py
CHANGED
|
@@ -1718,18 +1718,33 @@ def update_hub(
|
|
|
1718
1718
|
# A project that already exists on the hub, e.g., one created there
|
|
1719
1719
|
# for this repo, is connected to rather than created again, which the
|
|
1720
1720
|
# hub would refuse since a repo belongs to one project
|
|
1721
|
-
|
|
1722
|
-
if
|
|
1721
|
+
project = None
|
|
1722
|
+
if git_repo_url is not None and git_repo_url.startswith(
|
|
1723
|
+
"https://github.com/"
|
|
1724
|
+
):
|
|
1725
|
+
github_repo = git_repo_url.removeprefix("https://github.com/")
|
|
1726
|
+
try:
|
|
1727
|
+
found = calkit.hub.get(
|
|
1728
|
+
"/projects",
|
|
1729
|
+
params=dict(github_repo=github_repo),
|
|
1730
|
+
max_retries=1,
|
|
1731
|
+
)
|
|
1732
|
+
if found.get("data"):
|
|
1733
|
+
project = found["data"][0]
|
|
1734
|
+
except Exception:
|
|
1735
|
+
pass
|
|
1736
|
+
elif owner is not None:
|
|
1723
1737
|
try:
|
|
1724
|
-
calkit.hub.get(
|
|
1725
|
-
|
|
1738
|
+
project = calkit.hub.get(
|
|
1739
|
+
f"/projects/{owner}/{name}", max_retries=1
|
|
1740
|
+
)
|
|
1726
1741
|
except Exception:
|
|
1727
1742
|
pass
|
|
1728
1743
|
# Creating it is the step that can already be done, so a project that
|
|
1729
1744
|
# exists is not an error: the point is to end up connected
|
|
1730
1745
|
resp = None
|
|
1731
1746
|
try:
|
|
1732
|
-
if
|
|
1747
|
+
if project is not None:
|
|
1733
1748
|
raise RuntimeError("Project already exists")
|
|
1734
1749
|
resp = calkit.hub.post(
|
|
1735
1750
|
"/projects",
|
|
@@ -1760,6 +1775,22 @@ def update_hub(
|
|
|
1760
1775
|
typer.echo(f"Adding Git remote: {git_repo_url}")
|
|
1761
1776
|
repo.git.remote(["add", "origin", git_repo_url])
|
|
1762
1777
|
ck_info["git_repo_url"] = git_repo_url
|
|
1778
|
+
# The hub decides who owns the project, e.g., an org's repo belongs to
|
|
1779
|
+
# the org, under an account name that's lowercase even when the Git
|
|
1780
|
+
# remote's isn't
|
|
1781
|
+
for info in (project, resp):
|
|
1782
|
+
if isinstance(info, dict) and info.get("owner_account_name"):
|
|
1783
|
+
owner = info["owner_account_name"]
|
|
1784
|
+
name = info.get("name") or name
|
|
1785
|
+
if owner is not None and ck_info.get("owner") != owner:
|
|
1786
|
+
ck_info["owner"] = owner
|
|
1787
|
+
if ck_info.get("name") != name:
|
|
1788
|
+
ck_info["name"] = name
|
|
1789
|
+
if ck_info.get("hub") is None:
|
|
1790
|
+
ck_info["hub"] = hub_url
|
|
1791
|
+
# The DVC remote is named from calkit.yaml, so it has to be written first
|
|
1792
|
+
with open("calkit.yaml", "w") as f:
|
|
1793
|
+
calkit.ryaml.dump(ck_info, f)
|
|
1763
1794
|
try:
|
|
1764
1795
|
remote_name = configure_remote()
|
|
1765
1796
|
set_remote_auth(remote_name=remote_name)
|
|
@@ -1773,14 +1804,6 @@ def update_hub(
|
|
|
1773
1804
|
raise_error("Current directory is not a Git repository")
|
|
1774
1805
|
except (ValueError, RuntimeError) as e:
|
|
1775
1806
|
raise_error(f"Failed to set up DVC remote: {e}")
|
|
1776
|
-
if owner is not None and ck_info.get("owner") != owner:
|
|
1777
|
-
ck_info["owner"] = owner
|
|
1778
|
-
if ck_info.get("name") != name:
|
|
1779
|
-
ck_info["name"] = name
|
|
1780
|
-
if ck_info.get("hub") is None:
|
|
1781
|
-
ck_info["hub"] = hub_url
|
|
1782
|
-
with open("calkit.yaml", "w") as f:
|
|
1783
|
-
calkit.ryaml.dump(ck_info, f)
|
|
1784
1807
|
paths = [".dvc/config", "calkit.yaml"]
|
|
1785
1808
|
# The hub would have written one, and a repo it didn't scaffold has none
|
|
1786
1809
|
if not os.path.isfile("README.md"):
|
calkit/core.py
CHANGED
|
@@ -41,6 +41,82 @@ logger = logging.getLogger(__package__)
|
|
|
41
41
|
logger.setLevel(logging.INFO)
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
class _FoldedPlainStr(str):
|
|
45
|
+
"""A plain scalar read from several lines, remembering where they broke.
|
|
46
|
+
|
|
47
|
+
``fold_pos`` holds the index of each space that was a line break, so
|
|
48
|
+
the value can be written back the way it was wrapped rather than
|
|
49
|
+
re-folded at whatever width ruamel is set to, which rewrote every
|
|
50
|
+
paragraph of a file wrapped any other way.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
fold_pos: list[int]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class _FoldPreservingConstructor(ruamel.yaml.constructor.RoundTripConstructor):
|
|
57
|
+
def construct_scalar(self, node: Any) -> Any:
|
|
58
|
+
value = super().construct_scalar(node)
|
|
59
|
+
if (
|
|
60
|
+
type(value) is not str
|
|
61
|
+
or node.style is not None
|
|
62
|
+
or node.start_mark.line == node.end_mark.line
|
|
63
|
+
):
|
|
64
|
+
return value
|
|
65
|
+
# Only text loaded from a string has its source in the marks
|
|
66
|
+
buffer = getattr(node.start_mark, "buffer", None)
|
|
67
|
+
if buffer is None:
|
|
68
|
+
return value
|
|
69
|
+
source = buffer[node.start_mark.index : node.end_mark.index]
|
|
70
|
+
lines = [line.strip() for line in source.split("\n")]
|
|
71
|
+
# Blank lines are newlines in the value, which the emitter handles
|
|
72
|
+
if "" in lines or " ".join(lines) != value:
|
|
73
|
+
return value
|
|
74
|
+
out = _FoldedPlainStr(value)
|
|
75
|
+
out.fold_pos = []
|
|
76
|
+
pos = -1
|
|
77
|
+
for line in lines[:-1]:
|
|
78
|
+
pos += len(line) + 1
|
|
79
|
+
out.fold_pos.append(pos)
|
|
80
|
+
return out
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class _FoldPreservingRepresenter(ruamel.yaml.representer.RoundTripRepresenter):
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
_FoldPreservingRepresenter.add_representer(
|
|
88
|
+
_FoldedPlainStr, ruamel.yaml.representer.RoundTripRepresenter.represent_str
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class _FoldPreservingEmitter(ruamel.yaml.emitter.Emitter):
|
|
93
|
+
def write_plain(self, text: Any, split: Any = True) -> None:
|
|
94
|
+
fold_pos = getattr(text, "fold_pos", None)
|
|
95
|
+
if not fold_pos or not split or self.root_context:
|
|
96
|
+
return super().write_plain(text, split)
|
|
97
|
+
|
|
98
|
+
def write(data: str) -> None:
|
|
99
|
+
self.column += len(data)
|
|
100
|
+
self.stream.write(
|
|
101
|
+
data.encode(self.encoding) if self.encoding else data
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# The same bookkeeping as the parent's, breaking where the source
|
|
105
|
+
# did instead of where the width says to
|
|
106
|
+
if not self.whitespace:
|
|
107
|
+
write(" ")
|
|
108
|
+
self.whitespace = False
|
|
109
|
+
self.indention = False
|
|
110
|
+
start = 0
|
|
111
|
+
for pos in fold_pos:
|
|
112
|
+
write(text[start:pos])
|
|
113
|
+
self.write_indent()
|
|
114
|
+
self.whitespace = False
|
|
115
|
+
self.indention = False
|
|
116
|
+
start = pos + 1
|
|
117
|
+
write(text[start:])
|
|
118
|
+
|
|
119
|
+
|
|
44
120
|
class _ThreadLocalYAML(threading.local):
|
|
45
121
|
"""Holds one configured ruamel ``YAML`` per thread.
|
|
46
122
|
|
|
@@ -65,6 +141,10 @@ class _ThreadLocalYAML(threading.local):
|
|
|
65
141
|
# 79-80 columns is written back as it was; at 70, lines of 71-80
|
|
66
142
|
# characters were re-folded with a stray word on the next line
|
|
67
143
|
self.yaml.width = 80
|
|
144
|
+
# Prose wrapped some other way is still written back as it was
|
|
145
|
+
self.yaml.Constructor = _FoldPreservingConstructor
|
|
146
|
+
self.yaml.Representer = _FoldPreservingRepresenter
|
|
147
|
+
self.yaml.Emitter = _FoldPreservingEmitter
|
|
68
148
|
|
|
69
149
|
|
|
70
150
|
_yaml_local = _ThreadLocalYAML()
|
|
@@ -83,6 +163,20 @@ class _ThreadLocalYAMLProxy:
|
|
|
83
163
|
def __setattr__(self, name: str, value: Any) -> None:
|
|
84
164
|
setattr(_yaml_local.yaml, name, value)
|
|
85
165
|
|
|
166
|
+
def load(self, stream: Any) -> Any:
|
|
167
|
+
"""Load YAML, reading it to a string first.
|
|
168
|
+
|
|
169
|
+
Only a string load leaves the source in the marks, which is how
|
|
170
|
+
multi-line values remember where they were wrapped.
|
|
171
|
+
"""
|
|
172
|
+
from pathlib import Path
|
|
173
|
+
|
|
174
|
+
if isinstance(stream, Path):
|
|
175
|
+
stream = stream.read_text(encoding="utf-8")
|
|
176
|
+
elif hasattr(stream, "read"):
|
|
177
|
+
stream = stream.read()
|
|
178
|
+
return _yaml_local.yaml.load(stream)
|
|
179
|
+
|
|
86
180
|
def dump(self, data: Any, stream: Any = None, **kwargs: Any) -> Any:
|
|
87
181
|
"""Dump YAML, dropping the space ruamel leaves before a fold.
|
|
88
182
|
|
calkit/docx.py
CHANGED
|
@@ -633,10 +633,13 @@ class Document:
|
|
|
633
633
|
before: ET.Element,
|
|
634
634
|
rows: list[tuple[ET.Element, str | None, str]],
|
|
635
635
|
first_bid: int,
|
|
636
|
+
keep: dict[int, list[tuple[str, str]]] | None = None,
|
|
636
637
|
) -> None:
|
|
637
638
|
"""Insert display equations before a body element as a borderless
|
|
638
639
|
table of (math, number, bookmark name) rows, which Word and
|
|
639
640
|
LibreOffice both lay out like LaTeX: centered, numbered at right.
|
|
641
|
+
``keep`` is bookmarks, as (name, ID) by row, from what the
|
|
642
|
+
equations replace, so links to them still land.
|
|
640
643
|
"""
|
|
641
644
|
width = self.column_width(before)
|
|
642
645
|
side = width // 8
|
|
@@ -677,16 +680,19 @@ class Document:
|
|
|
677
680
|
)
|
|
678
681
|
# The bookmark spans the equation, since Word drops an
|
|
679
682
|
# empty one beside it
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
683
|
+
marks = [(name, str(first_bid + i))]
|
|
684
|
+
marks += (keep or {}).get(i, [])
|
|
685
|
+
for mark, bid in marks:
|
|
686
|
+
ET.SubElement(
|
|
687
|
+
p,
|
|
688
|
+
_tag(W, "bookmarkStart"),
|
|
689
|
+
{_tag(W, "id"): bid, _tag(W, "name"): mark},
|
|
690
|
+
)
|
|
686
691
|
ET.SubElement(p, _tag(M, "oMathPara")).append(math)
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
692
|
+
for _, bid in marks:
|
|
693
|
+
ET.SubElement(
|
|
694
|
+
p, _tag(W, "bookmarkEnd"), {_tag(W, "id"): bid}
|
|
695
|
+
)
|
|
690
696
|
elif j == 2 and number:
|
|
691
697
|
ET.SubElement(
|
|
692
698
|
ppr, _tag(W, "jc"), {_tag(W, "val"): "right"}
|
|
@@ -1120,6 +1126,44 @@ def find_soffice() -> str | None:
|
|
|
1120
1126
|
return next((c for c in candidates if os.path.isfile(c)), None)
|
|
1121
1127
|
|
|
1122
1128
|
|
|
1129
|
+
def _link_into_objects(content: str) -> str:
|
|
1130
|
+
"""Point links into embedded objects at the objects themselves.
|
|
1131
|
+
|
|
1132
|
+
TeX4ht puts each equation in an object of its own and links a
|
|
1133
|
+
reference to it to an anchor inside, e.g., ``main-m7.4om#x7-3001r2``,
|
|
1134
|
+
which Word can't follow, so LibreOffice writes it out as a path to a
|
|
1135
|
+
file in the build directory. A bookmark of that name beside the object
|
|
1136
|
+
in the main text makes it an ordinary link within the document.
|
|
1137
|
+
"""
|
|
1138
|
+
|
|
1139
|
+
def relink(m: re.Match) -> str:
|
|
1140
|
+
name, anchor = m.group("name"), m.group("anchor")
|
|
1141
|
+
if name not in objects:
|
|
1142
|
+
return str(m.group(0))
|
|
1143
|
+
anchors.setdefault(name, set()).add(anchor)
|
|
1144
|
+
return f"{m.group('attr')}{m.group('q')}#{anchor}{m.group('q')}"
|
|
1145
|
+
|
|
1146
|
+
def mark(m: re.Match) -> str:
|
|
1147
|
+
names = sorted(anchors.get(m.group("name"), ()))
|
|
1148
|
+
marks = "".join(f"<text:bookmark text:name='{a}'/>" for a in names)
|
|
1149
|
+
return marks + str(m.group(0))
|
|
1150
|
+
|
|
1151
|
+
frame_re = re.compile(
|
|
1152
|
+
r"<draw:frame\b(?:(?!</draw:frame>).)*?<draw:object\b[^>]*?"
|
|
1153
|
+
r"xlink:href=['\"]\./(?P<name>[^'\"/]+)['\"]",
|
|
1154
|
+
flags=re.S,
|
|
1155
|
+
)
|
|
1156
|
+
objects = {m.group("name") for m in frame_re.finditer(content)}
|
|
1157
|
+
anchors: dict[str, set[str]] = {}
|
|
1158
|
+
content = re.sub(
|
|
1159
|
+
r"(?P<attr><text:a\b[^>]*?xlink:href=)(?P<q>['\"])"
|
|
1160
|
+
r"(?P<name>[^'\"#/]+)\.[^'\"#./]+#(?P<anchor>[^'\"]+)(?P=q)",
|
|
1161
|
+
relink,
|
|
1162
|
+
content,
|
|
1163
|
+
)
|
|
1164
|
+
return frame_re.sub(mark, content)
|
|
1165
|
+
|
|
1166
|
+
|
|
1123
1167
|
def odt_to_docx(odt_path: str, docx_path: str) -> None:
|
|
1124
1168
|
"""Convert an OpenDocument text to .docx with LibreOffice."""
|
|
1125
1169
|
import shutil
|
|
@@ -1130,6 +1174,21 @@ def odt_to_docx(odt_path: str, docx_path: str) -> None:
|
|
|
1130
1174
|
if soffice is None:
|
|
1131
1175
|
raise RuntimeError("Converting to Word requires LibreOffice")
|
|
1132
1176
|
with tempfile.TemporaryDirectory() as tmp:
|
|
1177
|
+
# A copy with its links fixed up, leaving the original as made
|
|
1178
|
+
src = os.path.join(tmp, "src", Path(odt_path).name)
|
|
1179
|
+
os.makedirs(os.path.dirname(src))
|
|
1180
|
+
with (
|
|
1181
|
+
zipfile.ZipFile(odt_path) as zin,
|
|
1182
|
+
zipfile.ZipFile(src, "w") as zout,
|
|
1183
|
+
):
|
|
1184
|
+
for item in zin.infolist():
|
|
1185
|
+
data = zin.read(item)
|
|
1186
|
+
if item.filename == "content.xml":
|
|
1187
|
+
data = _link_into_objects(data.decode("utf-8")).encode(
|
|
1188
|
+
"utf-8"
|
|
1189
|
+
)
|
|
1190
|
+
zout.writestr(item, data)
|
|
1191
|
+
odt_path = src
|
|
1133
1192
|
# A profile of its own, since a LibreOffice already open would take
|
|
1134
1193
|
# the job and exit without doing it
|
|
1135
1194
|
res = subprocess.run(
|
calkit/dvc/core.py
CHANGED
|
@@ -367,6 +367,16 @@ class CalkitDVCFileSystem(ObjectFileSystem):
|
|
|
367
367
|
fs.info = cached_info # type: ignore[method-assign]
|
|
368
368
|
return fs
|
|
369
369
|
|
|
370
|
+
def _info_many(self, paths: list[str], **kwargs: Any) -> dict:
|
|
371
|
+
# Only a .dir is read once it's found, so any other object's content
|
|
372
|
+
# would be downloaded just to say it exists
|
|
373
|
+
dirs = [p for p in paths if p.endswith(".dir")]
|
|
374
|
+
others = [p for p in paths if not p.endswith(".dir")]
|
|
375
|
+
infos = self.fs.info_many(dirs, **kwargs) if dirs else {}
|
|
376
|
+
if others:
|
|
377
|
+
infos |= self.fs.info_many(others, content=False, **kwargs)
|
|
378
|
+
return infos
|
|
379
|
+
|
|
370
380
|
def _extract_owner_project(self) -> tuple[str, str] | None:
|
|
371
381
|
"""Extract owner and project from the path_info."""
|
|
372
382
|
try:
|
|
@@ -432,7 +442,7 @@ class CalkitDVCFileSystem(ObjectFileSystem):
|
|
|
432
442
|
uncached_paths.append(p)
|
|
433
443
|
# Only fetch info for uncached paths
|
|
434
444
|
if uncached_paths:
|
|
435
|
-
infos = self.
|
|
445
|
+
infos = self._info_many(uncached_paths, **kwargs)
|
|
436
446
|
# Cache the newly fetched info
|
|
437
447
|
for p in uncached_paths:
|
|
438
448
|
exists = p in infos and isinstance(infos[p], dict)
|
|
@@ -490,7 +500,7 @@ class CalkitDVCFileSystem(ObjectFileSystem):
|
|
|
490
500
|
# Only fetch info for uncached paths
|
|
491
501
|
# Use info_many to get and cache both info and existence
|
|
492
502
|
if uncached_paths:
|
|
493
|
-
infos = self.
|
|
503
|
+
infos = self._info_many(uncached_paths, **kwargs)
|
|
494
504
|
for p in uncached_paths:
|
|
495
505
|
exists = p in infos and isinstance(infos[p], dict)
|
|
496
506
|
if p not in self._cache:
|
calkit/fs.py
CHANGED
|
@@ -866,9 +866,13 @@ class CalkitFileSystem(AbstractFileSystem):
|
|
|
866
866
|
return self._get_info_for_parsed_path(owner, project, file_path)
|
|
867
867
|
|
|
868
868
|
def info_many(
|
|
869
|
-
self, paths: list[str], **kwargs
|
|
869
|
+
self, paths: list[str], content: bool = True, **kwargs
|
|
870
870
|
) -> dict[str, dict[str, Any]]:
|
|
871
|
-
"""Get metadata for multiple paths in a batch API call.
|
|
871
|
+
"""Get metadata for multiple paths in a batch API call.
|
|
872
|
+
|
|
873
|
+
With ``content``, each small file's bytes come back too, saving
|
|
874
|
+
a request per file for callers that will read them.
|
|
875
|
+
"""
|
|
872
876
|
if not paths:
|
|
873
877
|
return {}
|
|
874
878
|
grouped: dict[tuple[str, str], list[tuple[str, str]]] = {}
|
|
@@ -888,7 +892,7 @@ class CalkitFileSystem(AbstractFileSystem):
|
|
|
888
892
|
project=project,
|
|
889
893
|
paths=file_paths,
|
|
890
894
|
operation="info",
|
|
891
|
-
include=["info", "content"],
|
|
895
|
+
include=["info", "content"] if content else ["info"],
|
|
892
896
|
)
|
|
893
897
|
batch_results = resp.get("results")
|
|
894
898
|
if isinstance(batch_results, dict):
|
calkit/hub.py
CHANGED
|
@@ -40,6 +40,13 @@ _REFRESH_BUFFER_SECONDS = 60
|
|
|
40
40
|
# they don't recurse into the auth-retry paths in ``_request``.
|
|
41
41
|
_device_login_lock = threading.Lock()
|
|
42
42
|
|
|
43
|
+
# Reused so each request after the first skips the TLS handshake, which
|
|
44
|
+
# was most of the time of a small request. The pool is sized for DVC's
|
|
45
|
+
# concurrent transfers.
|
|
46
|
+
_session = requests.Session()
|
|
47
|
+
_session.mount("https://", requests.adapters.HTTPAdapter(pool_maxsize=64))
|
|
48
|
+
_session.mount("http://", requests.adapters.HTTPAdapter(pool_maxsize=64))
|
|
49
|
+
|
|
43
50
|
|
|
44
51
|
class DeviceLoginError(RuntimeError):
|
|
45
52
|
"""Raised when the OAuth device login flow cannot complete."""
|
|
@@ -381,7 +388,7 @@ def _request(
|
|
|
381
388
|
# Bound how long a single attempt can hang so stalled connections become
|
|
382
389
|
# retryable timeouts rather than blocking forever. Callers can override.
|
|
383
390
|
kwargs.setdefault("timeout", (10, 120))
|
|
384
|
-
func = getattr(
|
|
391
|
+
func = getattr(_session, kind)
|
|
385
392
|
if base_url is None:
|
|
386
393
|
base_url = get_base_url()
|
|
387
394
|
refresh_attempted = False
|
calkit/models/pipeline.py
CHANGED
|
@@ -228,6 +228,23 @@ def _allow_null(schema: dict[str, Any]) -> None:
|
|
|
228
228
|
schema["anyOf"] = [inner, {"type": "null"}]
|
|
229
229
|
|
|
230
230
|
|
|
231
|
+
def _unique_paths(paths: list[str]) -> list[str]:
|
|
232
|
+
"""Drop repeated paths, keeping the order they were first added in.
|
|
233
|
+
|
|
234
|
+
A stage can name the same path more than once: a document's target is
|
|
235
|
+
often also one of its inputs, and one source can be copied to several
|
|
236
|
+
destinations. DVC warns about a duplicate dep on every run, so the
|
|
237
|
+
generated stage lists each path once.
|
|
238
|
+
"""
|
|
239
|
+
seen: set[str] = set()
|
|
240
|
+
unique: list[str] = []
|
|
241
|
+
for path in paths:
|
|
242
|
+
if path not in seen:
|
|
243
|
+
seen.add(path)
|
|
244
|
+
unique.append(path)
|
|
245
|
+
return unique
|
|
246
|
+
|
|
247
|
+
|
|
231
248
|
class Stage(BaseModel):
|
|
232
249
|
"""A stage in the pipeline."""
|
|
233
250
|
|
|
@@ -709,6 +726,9 @@ class Stage(BaseModel):
|
|
|
709
726
|
path = i if isinstance(i, str) else i.path
|
|
710
727
|
if path not in deps:
|
|
711
728
|
deps.append(path)
|
|
729
|
+
# Each path appears once: a stage that names the same one twice, e.g.
|
|
730
|
+
# a target that is also an input, otherwise reaches DVC as a duplicate
|
|
731
|
+
deps = _unique_paths(deps)
|
|
712
732
|
outs = self.dvc_outs
|
|
713
733
|
log_out = self.scheduler_log_output
|
|
714
734
|
if log_out is not None:
|
|
@@ -954,7 +974,7 @@ class MapPathsStage(Stage):
|
|
|
954
974
|
deps = []
|
|
955
975
|
for path in self.paths:
|
|
956
976
|
deps.append(path.src)
|
|
957
|
-
return deps + super().dvc_deps
|
|
977
|
+
return _unique_paths(deps + super().dvc_deps)
|
|
958
978
|
|
|
959
979
|
@property
|
|
960
980
|
def dvc_outs(self) -> list[dict]:
|
|
@@ -1288,7 +1308,7 @@ class LatexStage(Stage):
|
|
|
1288
1308
|
deps = [self.target_path] + super().dvc_deps
|
|
1289
1309
|
if self.latexmkrc_path is not None:
|
|
1290
1310
|
deps.append(self.latexmkrc_path)
|
|
1291
|
-
return deps
|
|
1311
|
+
return _unique_paths(deps)
|
|
1292
1312
|
|
|
1293
1313
|
@property
|
|
1294
1314
|
def dvc_outs(self) -> list[str | dict]:
|
|
@@ -179,6 +179,9 @@ def test_execute_notebook(tmp_dir):
|
|
|
179
179
|
sys.platform == "win32",
|
|
180
180
|
reason="TODO: Julia env init fails on Windows GHA runners (Pkg stdlib missing)",
|
|
181
181
|
)
|
|
182
|
+
# Both install and build IJulia in the shared depot, and one precompiling it
|
|
183
|
+
# while the other builds it fails with "IJulia not properly installed"
|
|
184
|
+
@pytest.mark.xdist_group("ijulia")
|
|
182
185
|
def test_execute_notebook_julia(tmp_dir):
|
|
183
186
|
subprocess.check_call(
|
|
184
187
|
[
|
|
@@ -218,6 +221,9 @@ def test_execute_notebook_julia(tmp_dir):
|
|
|
218
221
|
sys.platform == "win32",
|
|
219
222
|
reason="TODO: Julia env init fails on Windows GHA runners (Pkg stdlib missing)",
|
|
220
223
|
)
|
|
224
|
+
# Both install and build IJulia in the shared depot, and one precompiling it
|
|
225
|
+
# while the other builds it fails with "IJulia not properly installed"
|
|
226
|
+
@pytest.mark.xdist_group("ijulia")
|
|
221
227
|
def test_check_env_kernel_julia(tmp_dir):
|
|
222
228
|
subprocess.check_call(["calkit", "init"])
|
|
223
229
|
subprocess.check_call(
|
calkit/tests/cli/test_update.py
CHANGED
|
@@ -23,7 +23,7 @@ def test_update_project_config(tmp_dir, monkeypatch):
|
|
|
23
23
|
|
|
24
24
|
import requests
|
|
25
25
|
|
|
26
|
-
monkeypatch.setattr(requests, "
|
|
26
|
+
monkeypatch.setattr(requests.Session, "request", fail)
|
|
27
27
|
subprocess.check_call(["calkit", "init"])
|
|
28
28
|
result = runner.invoke(update_app, ["devcontainer"])
|
|
29
29
|
assert result.exit_code == 0
|
|
@@ -561,24 +561,39 @@ def test_update_hub_creates_repo(tmp_dir, tmp_path_factory, monkeypatch):
|
|
|
561
561
|
posted: list[dict] = []
|
|
562
562
|
remote = {"url": ""}
|
|
563
563
|
|
|
564
|
+
posted_resp: dict = {}
|
|
565
|
+
|
|
564
566
|
def post(path, json=None, **kwargs):
|
|
565
567
|
posted.append(json)
|
|
566
|
-
return {"git_repo_url": remote["url"]}
|
|
568
|
+
return {"git_repo_url": remote["url"]} | posted_resp
|
|
567
569
|
|
|
568
570
|
existing: set[str] = set()
|
|
569
|
-
|
|
570
|
-
|
|
571
|
+
on_hub: dict[str, dict] = {}
|
|
572
|
+
|
|
573
|
+
def get(path, params=None, **kwargs):
|
|
574
|
+
if path == "/projects":
|
|
575
|
+
found = on_hub.get(params["github_repo"].lower())
|
|
576
|
+
return {
|
|
577
|
+
"data": [found] if found else [],
|
|
578
|
+
"count": int(bool(found)),
|
|
579
|
+
}
|
|
571
580
|
if path not in existing:
|
|
572
581
|
raise RuntimeError("404: Not found")
|
|
573
582
|
return {}
|
|
574
583
|
|
|
584
|
+
remote_names: list[str] = []
|
|
585
|
+
|
|
586
|
+
def configure_remote() -> str:
|
|
587
|
+
remote_names.append(calkit.detect_project_name())
|
|
588
|
+
return "calkit"
|
|
589
|
+
|
|
575
590
|
monkeypatch.setattr(calkit.hub, "post", post)
|
|
576
591
|
monkeypatch.setattr(calkit.hub, "get", get)
|
|
577
592
|
monkeypatch.setattr(
|
|
578
593
|
calkit.hub, "get_current_user", lambda: {"github_username": "someone"}
|
|
579
594
|
)
|
|
580
595
|
monkeypatch.setattr(calkit.hub, "get_hub_url", lambda: "https://hub.test")
|
|
581
|
-
monkeypatch.setattr(calkit.dvc, "configure_remote",
|
|
596
|
+
monkeypatch.setattr(calkit.dvc, "configure_remote", configure_remote)
|
|
582
597
|
monkeypatch.setattr(
|
|
583
598
|
calkit.dvc, "set_remote_auth", lambda remote_name: None
|
|
584
599
|
)
|
|
@@ -626,3 +641,34 @@ def test_update_hub_creates_repo(tmp_dir, tmp_path_factory, monkeypatch):
|
|
|
626
641
|
assert "already exists" in result.output
|
|
627
642
|
assert len(posted) == n_posted
|
|
628
643
|
assert calkit.load_calkit_info()["hub"] == "https://hub.test"
|
|
644
|
+
# An org's repo already on the hub is found by its URL, and the remote
|
|
645
|
+
# is named for the account that owns it there, which is lowercase
|
|
646
|
+
on_hub["acme-lab/widget"] = {
|
|
647
|
+
"owner_account_name": "acme-lab",
|
|
648
|
+
"name": "widget",
|
|
649
|
+
}
|
|
650
|
+
git("remote", "set-url", "origin", "git@github.com:Acme-Lab/widget.git")
|
|
651
|
+
with open("calkit.yaml", "w") as f:
|
|
652
|
+
f.write("title: Widget\n")
|
|
653
|
+
result = runner.invoke(
|
|
654
|
+
update_app, ["hub", "https://hub.test", "--no-commit"]
|
|
655
|
+
)
|
|
656
|
+
assert result.exit_code == 0, result.output
|
|
657
|
+
assert len(posted) == n_posted
|
|
658
|
+
assert remote_names[-1] == "acme-lab/widget"
|
|
659
|
+
ck_info = calkit.load_calkit_info()
|
|
660
|
+
assert ck_info["owner"] == "acme-lab"
|
|
661
|
+
assert ck_info["name"] == "widget"
|
|
662
|
+
assert "https://hub.test/acme-lab/widget" in result.output
|
|
663
|
+
# One the client creates takes the owner the hub gave it
|
|
664
|
+
on_hub.clear()
|
|
665
|
+
with open("calkit.yaml", "w") as f:
|
|
666
|
+
f.write("name: widget\ntitle: Widget\n")
|
|
667
|
+
posted_resp["owner_account_name"] = "acme-lab"
|
|
668
|
+
result = runner.invoke(
|
|
669
|
+
update_app, ["hub", "https://hub.test", "--no-commit"]
|
|
670
|
+
)
|
|
671
|
+
assert result.exit_code == 0, result.output
|
|
672
|
+
assert posted[-1]["git_repo_url"] == "https://github.com/Acme-Lab/widget"
|
|
673
|
+
assert remote_names[-1] == "acme-lab/widget"
|
|
674
|
+
assert calkit.load_calkit_info()["owner"] == "acme-lab"
|
calkit/tests/dvc/test_core.py
CHANGED
|
@@ -586,3 +586,48 @@ def test_commit_path_with_missing_dep(tmp_dir):
|
|
|
586
586
|
# The output's hash should be kept, since there's nothing new to hash
|
|
587
587
|
assert lock["stages"]["my-stage"]["outs"][0]["path"] == "output.txt"
|
|
588
588
|
assert "md5" in lock["stages"]["my-stage"]["outs"][0]
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def test_ck_fs_reads_content_only_for_dirs() -> None:
|
|
592
|
+
from calkit.dvc.core import CalkitDVCFileSystem
|
|
593
|
+
|
|
594
|
+
calls: list[tuple[list[str], bool]] = []
|
|
595
|
+
stored = {
|
|
596
|
+
"o/p/files/md5/aa/1.dir": b"[]",
|
|
597
|
+
"o/p/files/md5/bb/2": b"data",
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
class FakeFS:
|
|
601
|
+
def info_many(
|
|
602
|
+
self, paths: list[str], content: bool = True, **kwargs: object
|
|
603
|
+
) -> dict[str, dict]:
|
|
604
|
+
calls.append((sorted(paths), content))
|
|
605
|
+
infos = {}
|
|
606
|
+
for path in paths:
|
|
607
|
+
if path in stored:
|
|
608
|
+
infos[path] = {"name": path, "size": len(stored[path])}
|
|
609
|
+
if content:
|
|
610
|
+
infos[path]["content"] = stored[path]
|
|
611
|
+
return infos
|
|
612
|
+
|
|
613
|
+
fs = CalkitDVCFileSystem()
|
|
614
|
+
fs.__dict__["fs"] = FakeFS()
|
|
615
|
+
paths = [
|
|
616
|
+
"o/p/files/md5/aa/1.dir",
|
|
617
|
+
"o/p/files/md5/bb/2",
|
|
618
|
+
"o/p/files/md5/cc/3.dir",
|
|
619
|
+
"o/p/files/md5/dd/4",
|
|
620
|
+
]
|
|
621
|
+
assert fs.exists(paths) == [True, True, False, False]
|
|
622
|
+
assert calls == [
|
|
623
|
+
(["o/p/files/md5/aa/1.dir", "o/p/files/md5/cc/3.dir"], True),
|
|
624
|
+
(["o/p/files/md5/bb/2", "o/p/files/md5/dd/4"], False),
|
|
625
|
+
]
|
|
626
|
+
infos = fs.info(paths[:2])
|
|
627
|
+
assert infos[0]["content"] == b"[]"
|
|
628
|
+
assert "content" not in infos[1]
|
|
629
|
+
# Everything asked about is answered from what was already fetched
|
|
630
|
+
assert len(calls) == 2
|
|
631
|
+
with pytest.raises(FileNotFoundError):
|
|
632
|
+
fs.info(paths)
|
|
633
|
+
assert len(calls) == 2
|
|
@@ -17,6 +17,7 @@ from calkit.models.pipeline import (
|
|
|
17
17
|
Pipeline,
|
|
18
18
|
PythonScriptStage,
|
|
19
19
|
QuartoStage,
|
|
20
|
+
ShellScriptStage,
|
|
20
21
|
StageIteration,
|
|
21
22
|
WordToPdfStage,
|
|
22
23
|
)
|
|
@@ -1005,3 +1006,61 @@ def test_system_env_can_wrap_a_runtime():
|
|
|
1005
1006
|
bad.set_stage_scheduler_options(
|
|
1006
1007
|
envs | {"sched": {"kind": "slurm", "host": "hpc.edu"}}
|
|
1007
1008
|
)
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
def test_dvc_deps_are_unique_per_stage():
|
|
1012
|
+
# A generated dvc.yaml lists each dependency once. A stage that names the
|
|
1013
|
+
# same path twice otherwise reaches DVC as a duplicate, which it warns
|
|
1014
|
+
# about on every run
|
|
1015
|
+
def assert_unique(deps: list[str]) -> None:
|
|
1016
|
+
assert len(deps) == len(set(deps)), f"duplicates in {deps}"
|
|
1017
|
+
|
|
1018
|
+
# The target is also a common input, e.g. a bib or a figure the document
|
|
1019
|
+
# reads, so it arrives as both target_path and an input
|
|
1020
|
+
latex = LatexStage(
|
|
1021
|
+
name="paper",
|
|
1022
|
+
environment="tex",
|
|
1023
|
+
target_path="main.tex",
|
|
1024
|
+
inputs=["main.tex", "figures/plot.png"],
|
|
1025
|
+
)
|
|
1026
|
+
assert_unique(latex.dvc_deps)
|
|
1027
|
+
# One source copied to two destinations is one dependency, not two
|
|
1028
|
+
mapped = MapPathsStage(
|
|
1029
|
+
name="copy",
|
|
1030
|
+
paths=[
|
|
1031
|
+
dict(kind="file-to-file", src="paper/main.tex", dest="out/a.tex"),
|
|
1032
|
+
dict(kind="file-to-file", src="paper/main.tex", dest="out/b.tex"),
|
|
1033
|
+
],
|
|
1034
|
+
)
|
|
1035
|
+
assert_unique(mapped.dvc_deps)
|
|
1036
|
+
# The compiled stage is what reaches dvc.yaml
|
|
1037
|
+
for stage in [latex, mapped]:
|
|
1038
|
+
assert_unique(stage.to_dvc()["deps"])
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
def test_to_dvc_deps_have_no_duplicates():
|
|
1042
|
+
# A script named as its own input reaches the generated stage twice:
|
|
1043
|
+
# once through the script_path override and once through inputs
|
|
1044
|
+
for stage in [
|
|
1045
|
+
PythonScriptStage(
|
|
1046
|
+
name="run",
|
|
1047
|
+
environment="py",
|
|
1048
|
+
script_path="run.py",
|
|
1049
|
+
inputs=["run.py", "data/in.csv"],
|
|
1050
|
+
),
|
|
1051
|
+
ShellScriptStage(
|
|
1052
|
+
name="shell",
|
|
1053
|
+
kind="shell-script",
|
|
1054
|
+
environment="_system",
|
|
1055
|
+
script_path="run.sh",
|
|
1056
|
+
inputs=["run.sh"],
|
|
1057
|
+
),
|
|
1058
|
+
WordToPdfStage(
|
|
1059
|
+
name="doc",
|
|
1060
|
+
environment="tex",
|
|
1061
|
+
word_doc_path="document.docx",
|
|
1062
|
+
inputs=["document.docx"],
|
|
1063
|
+
),
|
|
1064
|
+
]:
|
|
1065
|
+
deps = stage.to_dvc()["deps"]
|
|
1066
|
+
assert len(deps) == len(set(deps)), f"duplicates in {deps}"
|
calkit/tests/test_core.py
CHANGED
|
@@ -364,6 +364,56 @@ def test_ryaml_dump_keeps_significant_trailing_whitespace():
|
|
|
364
364
|
assert calkit.ryaml.load(buf.getvalue()) == {"k": value}
|
|
365
365
|
|
|
366
366
|
|
|
367
|
+
@pytest.mark.usefixtures("tmp_dir")
|
|
368
|
+
def test_ryaml_keeps_how_prose_was_wrapped() -> None:
|
|
369
|
+
import copy
|
|
370
|
+
import io
|
|
371
|
+
from pathlib import Path
|
|
372
|
+
|
|
373
|
+
import calkit
|
|
374
|
+
|
|
375
|
+
def dumped(data: dict) -> str:
|
|
376
|
+
buf = io.StringIO()
|
|
377
|
+
calkit.ryaml.dump(data, buf)
|
|
378
|
+
return buf.getvalue()
|
|
379
|
+
|
|
380
|
+
# Wrapped narrower than ruamel's width, which used to re-fold it all
|
|
381
|
+
text = (
|
|
382
|
+
"title: A project\n"
|
|
383
|
+
"questions:\n"
|
|
384
|
+
" - question: Does the new mesh change the drag coefficient by\n"
|
|
385
|
+
" more than the measurement uncertainty?\n"
|
|
386
|
+
" notes: The coarse and fine meshes agree to within two\n"
|
|
387
|
+
" percent, so the remaining difference comes from the\n"
|
|
388
|
+
" turbulence model rather than the resolution.\n"
|
|
389
|
+
)
|
|
390
|
+
with open("calkit.yaml", "w") as f:
|
|
391
|
+
f.write(text)
|
|
392
|
+
|
|
393
|
+
# However it's loaded, an untouched file is written back as it was
|
|
394
|
+
with open("calkit.yaml") as f:
|
|
395
|
+
from_file = calkit.ryaml.load(f)
|
|
396
|
+
for data in [
|
|
397
|
+
calkit.ryaml.load(text),
|
|
398
|
+
from_file,
|
|
399
|
+
calkit.ryaml.load(Path("calkit.yaml")),
|
|
400
|
+
copy.deepcopy(from_file),
|
|
401
|
+
]:
|
|
402
|
+
assert dumped(data) == text
|
|
403
|
+
info = calkit.load_calkit_info()
|
|
404
|
+
info["owner"] = "someone"
|
|
405
|
+
calkit.save_calkit_info(info)
|
|
406
|
+
with open("calkit.yaml") as f:
|
|
407
|
+
assert f.read() == text + "owner: someone\n"
|
|
408
|
+
# A value that's changed is wrapped at the width like any other
|
|
409
|
+
data = calkit.ryaml.load(text)
|
|
410
|
+
data["questions"][0]["notes"] += " And one more sentence to fold."
|
|
411
|
+
out = dumped(data)
|
|
412
|
+
assert "agree to within two\n" not in out
|
|
413
|
+
assert calkit.ryaml.load(out) == data
|
|
414
|
+
assert " more than the measurement uncertainty?\n" in out
|
|
415
|
+
|
|
416
|
+
|
|
367
417
|
def test_update_readme_content():
|
|
368
418
|
import calkit
|
|
369
419
|
|
calkit/tests/test_docx.py
CHANGED
|
@@ -15,6 +15,7 @@ import re
|
|
|
15
15
|
import shutil
|
|
16
16
|
import subprocess
|
|
17
17
|
from pathlib import Path
|
|
18
|
+
from xml.etree import ElementTree as ET
|
|
18
19
|
|
|
19
20
|
import pytest
|
|
20
21
|
|
|
@@ -604,6 +605,13 @@ def test_docx_round_trip(
|
|
|
604
605
|
assert lo_sent is not None and len(lo_sent.equations) == 1
|
|
605
606
|
assert len(lo_sent.paragraphs) >= 15
|
|
606
607
|
assert any(p.text == "(1)" for p in lo.paragraphs)
|
|
608
|
+
# A reference to an equation links to it inside the document, not
|
|
609
|
+
# to TeX4ht's build directory
|
|
610
|
+
body = lo.parts["word/document.xml"].decode("utf-8")
|
|
611
|
+
anchors = re.findall(r'w:anchor="([^"]+)"', body)
|
|
612
|
+
marks = set(re.findall(r'w:bookmarkStart [^>]*w:name="([^"]+)"', body))
|
|
613
|
+
assert anchors and set(anchors) <= marks
|
|
614
|
+
assert "HYPERLINK" not in body and ".4om" not in body
|
|
607
615
|
main = Path("paper/main.tex").read_text(encoding="utf-8")
|
|
608
616
|
methods = Path("paper/methods.tex").read_text(encoding="utf-8")
|
|
609
617
|
res = subprocess.run(
|
|
@@ -615,3 +623,48 @@ def test_docx_round_trip(
|
|
|
615
623
|
assert "Applied 0 edits" in res.stdout
|
|
616
624
|
assert Path("paper/main.tex").read_text(encoding="utf-8") == main
|
|
617
625
|
assert Path("paper/methods.tex").read_text(encoding="utf-8") == methods
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def test_equation_links() -> None:
|
|
629
|
+
content = (
|
|
630
|
+
"<text:p>See (<text:a xlink:href='main-m7.4om#x7-3001r2' "
|
|
631
|
+
"xlink:type='simple'>2</text:a>), "
|
|
632
|
+
'<text:a xlink:href="main-m7.4om#x7-3001r3">3</text:a>, '
|
|
633
|
+
"<text:a xlink:href='#Xsmith2020'>Smith</text:a>, "
|
|
634
|
+
"<text:a xlink:href='https://calkit.io/a.html#b'>web</text:a> and "
|
|
635
|
+
"<text:a xlink:href='other.4om#x9'>gone</text:a>.</text:p>"
|
|
636
|
+
"<text:p><draw:frame draw:name='obj-7'><draw:object "
|
|
637
|
+
"xlink:href='./main-m7' xlink:show='embed'/></draw:frame></text:p>"
|
|
638
|
+
)
|
|
639
|
+
out = calkit.docx._link_into_objects(content)
|
|
640
|
+
# Links into an object point at bookmarks beside it
|
|
641
|
+
assert "xlink:href='#x7-3001r2'" in out
|
|
642
|
+
assert 'xlink:href="#x7-3001r3"' in out
|
|
643
|
+
assert (
|
|
644
|
+
"<text:p><text:bookmark text:name='x7-3001r2'/>"
|
|
645
|
+
"<text:bookmark text:name='x7-3001r3'/><draw:frame" in out
|
|
646
|
+
)
|
|
647
|
+
# Links within the document or out of it, or to an object that isn't
|
|
648
|
+
# there, are left alone
|
|
649
|
+
assert "xlink:href='#Xsmith2020'" in out
|
|
650
|
+
assert "xlink:href='https://calkit.io/a.html#b'" in out
|
|
651
|
+
assert "xlink:href='other.4om#x9'" in out
|
|
652
|
+
assert calkit.docx._link_into_objects(out) == out
|
|
653
|
+
# A kept bookmark goes on the row it's for, not just the first
|
|
654
|
+
doc = calkit.docx.Document(str(FIXTURES / "export.docx"))
|
|
655
|
+
w, m = calkit.docx.W, calkit.docx.M
|
|
656
|
+
before = next(doc.doc.iter(calkit.docx._tag(w, "p")))
|
|
657
|
+
rows = [
|
|
658
|
+
(ET.Element(calkit.docx._tag(m, "oMath")), "4", "ck_a"),
|
|
659
|
+
(ET.Element(calkit.docx._tag(m, "oMath")), "5", "ck_a_r2"),
|
|
660
|
+
]
|
|
661
|
+
doc.insert_equations(before, rows, 100, keep={1: [("x13-3004r5", "7")]})
|
|
662
|
+
tbl = next(doc.doc.iter(calkit.docx._tag(w, "tbl")))
|
|
663
|
+
names = [
|
|
664
|
+
[
|
|
665
|
+
b.get(calkit.docx._tag(w, "name"))
|
|
666
|
+
for b in tr.iter(calkit.docx._tag(w, "bookmarkStart"))
|
|
667
|
+
]
|
|
668
|
+
for tr in tbl.iter(calkit.docx._tag(w, "tr"))
|
|
669
|
+
]
|
|
670
|
+
assert names == [["ck_a"], ["ck_a_r2", "x13-3004r5"]]
|
calkit/tests/test_hub.py
CHANGED
|
@@ -294,7 +294,7 @@ def test_request_retries_on_401_with_refresh(monkeypatch):
|
|
|
294
294
|
return Resp401()
|
|
295
295
|
return Resp200()
|
|
296
296
|
|
|
297
|
-
monkeypatch.setattr(hub.
|
|
297
|
+
monkeypatch.setattr(hub._session, "get", _fake_get)
|
|
298
298
|
monkeypatch.setitem(hub._tokens, base_url, fresh)
|
|
299
299
|
monkeypatch.setattr(hub, "_try_refresh", lambda: fresh)
|
|
300
300
|
result = hub._request("get", "/test", base_url=base_url)
|
|
@@ -333,7 +333,7 @@ def test_request_invalid_credentials_403_triggers_refresh(monkeypatch):
|
|
|
333
333
|
call_count["n"] += 1
|
|
334
334
|
return Resp403() if call_count["n"] == 1 else Resp200()
|
|
335
335
|
|
|
336
|
-
monkeypatch.setattr(hub.
|
|
336
|
+
monkeypatch.setattr(hub._session, "get", _fake_get)
|
|
337
337
|
monkeypatch.setitem(hub._tokens, base_url, fresh)
|
|
338
338
|
monkeypatch.setattr(hub, "_try_refresh", lambda: fresh)
|
|
339
339
|
result = hub._request("get", "/test", base_url=base_url)
|
|
@@ -369,7 +369,7 @@ def test_request_permission_403_does_not_trigger_refresh(monkeypatch):
|
|
|
369
369
|
refresh_calls["n"] += 1
|
|
370
370
|
return fresh
|
|
371
371
|
|
|
372
|
-
monkeypatch.setattr(hub.
|
|
372
|
+
monkeypatch.setattr(hub._session, "post", _fake_post)
|
|
373
373
|
monkeypatch.setitem(hub._tokens, base_url, fresh)
|
|
374
374
|
monkeypatch.setattr(hub, "_try_refresh", _fake_refresh)
|
|
375
375
|
with pytest.raises(Exception):
|
|
@@ -410,7 +410,7 @@ def test_request_retries_on_transient_5xx(monkeypatch):
|
|
|
410
410
|
calls["n"] += 1
|
|
411
411
|
return Resp(status)
|
|
412
412
|
|
|
413
|
-
monkeypatch.setattr(hub.
|
|
413
|
+
monkeypatch.setattr(hub._session, "get", _fake_get)
|
|
414
414
|
result = hub._request("get", "/test", base_url=base_url)
|
|
415
415
|
assert result == {"ok": True}
|
|
416
416
|
assert calls["n"] == 2
|
|
@@ -421,7 +421,7 @@ def test_request_retries_on_transient_5xx(monkeypatch):
|
|
|
421
421
|
persistent["n"] += 1
|
|
422
422
|
return Resp(500)
|
|
423
423
|
|
|
424
|
-
monkeypatch.setattr(hub.
|
|
424
|
+
monkeypatch.setattr(hub._session, "get", _fake_get_500)
|
|
425
425
|
with pytest.raises(HTTPError):
|
|
426
426
|
hub._request("get", "/test", base_url=base_url)
|
|
427
427
|
# Initial attempt plus max_retries follow-ups.
|
|
@@ -458,7 +458,7 @@ def test_request_retries_on_network_error(monkeypatch):
|
|
|
458
458
|
raise errors[calls["n"] - 1]
|
|
459
459
|
return Resp200()
|
|
460
460
|
|
|
461
|
-
monkeypatch.setattr(hub.
|
|
461
|
+
monkeypatch.setattr(hub._session, "get", _fake_get)
|
|
462
462
|
assert hub._request("get", "/test", base_url=base_url) == {"ok": True}
|
|
463
463
|
assert calls["n"] == 3
|
|
464
464
|
# Case 2: a persistent network error exhausts retries and propagates.
|
|
@@ -468,7 +468,7 @@ def test_request_retries_on_network_error(monkeypatch):
|
|
|
468
468
|
persistent["n"] += 1
|
|
469
469
|
raise Timeout("read timeout")
|
|
470
470
|
|
|
471
|
-
monkeypatch.setattr(hub.
|
|
471
|
+
monkeypatch.setattr(hub._session, "get", _always_timeout)
|
|
472
472
|
with pytest.raises(Timeout):
|
|
473
473
|
hub._request("get", "/test", base_url=base_url)
|
|
474
474
|
assert persistent["n"] == 11
|
|
@@ -533,7 +533,7 @@ def test_request_can_skip_retries_and_login(monkeypatch):
|
|
|
533
533
|
attempts["n"] += 1
|
|
534
534
|
return _Resp()
|
|
535
535
|
|
|
536
|
-
monkeypatch.setattr(calkit.hub.
|
|
536
|
+
monkeypatch.setattr(calkit.hub._session, "post", fake_post)
|
|
537
537
|
monkeypatch.setattr(calkit.hub, "get_headers", lambda h, auth=True: {})
|
|
538
538
|
monkeypatch.setattr(calkit.hub, "get_base_url", lambda: "http://x")
|
|
539
539
|
with pytest.raises(calkit.hub.HTTPError):
|
|
@@ -3,18 +3,18 @@ calkit/__main__.py,sha256=whqS7I7q_c9004emvKptZ9B9OVn-jgr5kaLaCFKmwa0,112
|
|
|
3
3
|
calkit/calc.py,sha256=5RQVwH3goakmeDwHtmslcAZJXEZihIBuLNw_lF6QCcc,7924
|
|
4
4
|
calkit/conda.py,sha256=ddsHScMlPkpPU-d5vi-X--ANFvO2fi0fRFUFd_Ux51Y,33399
|
|
5
5
|
calkit/config.py,sha256=kMGYi-S4Ojp_VRGgxapvsci3XYcIBx3jGllmdLe7wuo,21780
|
|
6
|
-
calkit/core.py,sha256=
|
|
6
|
+
calkit/core.py,sha256=HBG4gho491y5tZL_VTXo_HFB-sIZnoKhuFWQ5X0Kplc,58092
|
|
7
7
|
calkit/datasets.py,sha256=3YtDmvBCzyX5AC-7Mb7ylnxM1mFLyTLj5CH7QqblDwA,2212
|
|
8
8
|
calkit/dependencies.py,sha256=MI_wuvQaY9okkmui37qz2yIxxBXJ3yuJUOQRj95IEbw,15362
|
|
9
9
|
calkit/detect.py,sha256=qEdYOrXMVCUYRoQ7YBQZBn_YZQT3K2GeWSOnWceGpNA,88418
|
|
10
10
|
calkit/docker.py,sha256=kMMcmAsVIuvYlANiVow2DuLTkMKJBN8HabyW5UsgOYs,50668
|
|
11
|
-
calkit/docx.py,sha256=
|
|
11
|
+
calkit/docx.py,sha256=wP0vwzcTdRDknUyaelr1_OVgW8C6RDnfZHKKRPXWp4k,55141
|
|
12
12
|
calkit/environments.py,sha256=O10Ppy2Sy0IKzFYoAJRbdxELBDG04u6p9zMKWuRh0-8,102536
|
|
13
|
-
calkit/fs.py,sha256=
|
|
13
|
+
calkit/fs.py,sha256=7huOJIDPni4bTJ3MZyyifgE4NqbfbT_bPW8Uff-7JMU,45658
|
|
14
14
|
calkit/git.py,sha256=ikSKyvx-xuk64deSeBTDqXsNlaFVePlP8Vc_Zupd7IU,27324
|
|
15
15
|
calkit/github.py,sha256=nBR3CH3mFIDk_atWtZ_EdQEP3ktDi-xNPbe6Bbusarg,1699
|
|
16
16
|
calkit/gui.py,sha256=2UCrMyosc5v4CLlDcHO7oDwW9eLn5_WbNaLaG5bjMTc,23
|
|
17
|
-
calkit/hub.py,sha256=
|
|
17
|
+
calkit/hub.py,sha256=cZ8ltzambJ9MObb5Xj1B8MblmO0XVws7Y1MoOO0gWlM,19959
|
|
18
18
|
calkit/install.py,sha256=y3-ZUZt1Tf3H9mP5v8dXtrNnhx7NiJPolF9ptlWRUkk,16997
|
|
19
19
|
calkit/invenio.py,sha256=i3UrEkzWXw4buhVwgjpLJL4AierBYnyWJCg0ytLuaBo,6214
|
|
20
20
|
calkit/julia.py,sha256=4GxUSItNXpYcRlCD9N8dLiuX44-HQaKFcuEudD8At1I,5640
|
|
@@ -47,7 +47,7 @@ calkit/cli/describe.py,sha256=LMODpla_g7hyTI7Oi7V5dAb9WSp5nZ_4-XhSYmZP1eo,3608
|
|
|
47
47
|
calkit/cli/dev.py,sha256=_BZqDdfF1VSfXaFfdBvofwE6cpcevhbgjRYNUHRQU2M,1761
|
|
48
48
|
calkit/cli/hub.py,sha256=ZBOlL7-mV57q_ynr4hPuhV7NlkKEkINSRksXD-jjiOg,2972
|
|
49
49
|
calkit/cli/import_.py,sha256=QOZEfKUmqMntle47PRZhN8cs7gfWCBte_smPZqksl54,25563
|
|
50
|
-
calkit/cli/latex.py,sha256=
|
|
50
|
+
calkit/cli/latex.py,sha256=CXpPsu8359pUR7fJkXosRFaiag8aLa1YAk_GUCf04Y0,111959
|
|
51
51
|
calkit/cli/list.py,sha256=dcpDdeG8OdjL5--DbE27URXQnuB4w1pPYltsBFC45_s,21969
|
|
52
52
|
calkit/cli/new.py,sha256=aGPHpz8poGt_F04sdaKsvOLr4Kowc0qaV-n4dhO6VPM,158819
|
|
53
53
|
calkit/cli/notebooks.py,sha256=aR4I0qatOtkrAxEG5PVLej20SBLKo8qr-lxDSzgljUQ,38836
|
|
@@ -56,12 +56,12 @@ calkit/cli/overleaf.py,sha256=vuV0GktH2ZlI93s-pA7RZ4zRFM9BLUC20lMrVZ92mro,38727
|
|
|
56
56
|
calkit/cli/scheduler.py,sha256=s4q9wLcvTCmxuLmAoz7ptJZiNXPWCamn-NU1S5l28go,52719
|
|
57
57
|
calkit/cli/show.py,sha256=zI9dqviFxXlcjnpEXnN7Ip2kmw9bVwP74rJDM9UqZ4s,3074
|
|
58
58
|
calkit/cli/sync.py,sha256=o6EnwADaUUK1yudWTc0gllKNLYD54jJbMnYBIuG5SfY,12643
|
|
59
|
-
calkit/cli/update.py,sha256=
|
|
59
|
+
calkit/cli/update.py,sha256=CfjghxEQIB8dJ7XahH64kroULFhQHLJqRAiOKNEOwZY,64595
|
|
60
60
|
calkit/cli/main/__init__.py,sha256=qu1POZPyqs33ZKfasOxv_Wc-EzVcEgK3Dt_vwFL8Bi8,65
|
|
61
61
|
calkit/cli/main/core.py,sha256=cHeRj62MREnc6_t7FFUs2mtu-vl6UiVtyYelmrXdF7g,179890
|
|
62
62
|
calkit/cli/main/xr.py,sha256=cYQBNVlRTzp6ba3EwEwZylOxVpk9ThWhy6IAD7_ahWw,37594
|
|
63
63
|
calkit/dvc/__init__.py,sha256=-B6tilCb_jw7QFmJ2srILez24wDhNKUzIBhZUMt01dE,381
|
|
64
|
-
calkit/dvc/core.py,sha256=
|
|
64
|
+
calkit/dvc/core.py,sha256=RadL_0va0kIaalIkDLzCa-KPbiOltlR1ADFPKtsP_ro,45866
|
|
65
65
|
calkit/dvc/zip.py,sha256=8_FdpEeQ5N3mbq0nwqm69F3enOHsQkSZpx2KBYLbSCg,21888
|
|
66
66
|
calkit/jupyterlab/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
67
67
|
calkit/jupyterlab/routes.py,sha256=sARrFjWnHVV-LunrgqO4DCt_x7XLhVPAqA1eaUFf7HQ,51443
|
|
@@ -70,7 +70,7 @@ calkit/models/core.py,sha256=mL9ImXwt1iTtDKzdmt96F9ONevLSTWhIKb_xAEJFb5I,80570
|
|
|
70
70
|
calkit/models/docx.py,sha256=pVfBSmb2pfe-AeDYhJuYCvGohE98ixDuBBbkmtDyOMU,2897
|
|
71
71
|
calkit/models/io.py,sha256=0nup-gDz3FbiD8AT5pTpxji6XN_rW5AsijErsIFdpbg,2330
|
|
72
72
|
calkit/models/iteration.py,sha256=_QajYhWVXV9ge9naX8ZkT1PBNzfc75cpl2BR2f8VPOo,2945
|
|
73
|
-
calkit/models/pipeline.py,sha256=
|
|
73
|
+
calkit/models/pipeline.py,sha256=6JiABNJDx_MgcsswT8AgNiY2R3ff3cqt6cFZ22CCPfM,100460
|
|
74
74
|
calkit/resources/README.md,sha256=sqqGXkhVLGeo50cXHx6XDm_yYB3klyDhudisZlLpNuE,1463
|
|
75
75
|
calkit/resources/__init__.py,sha256=QhDLrihdVQGyW2xEGzj2fIHGheq6Pn8W8jwlrjmmRv0,5122
|
|
76
76
|
calkit/resources/devcontainer/Dockerfile,sha256=U56BhC7sB9qlwpSMMb8CNHgNyuF-ZCV7X7E88rx6KQQ,3091
|
|
@@ -101,16 +101,16 @@ calkit/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
101
101
|
calkit/tests/test_calc.py,sha256=h-wOUIRqfyi06LTzhY_rS-a1xChZTkmwdEfBybMUM7E,2033
|
|
102
102
|
calkit/tests/test_conda.py,sha256=aJzuKHT81DfbmcRpae-x0I8s6I-ZTdbSlq5yjIYIvTM,17097
|
|
103
103
|
calkit/tests/test_config.py,sha256=hPJ-PRDOXjrwwO_PcP8KG1ehlrlHUI2Qhlkja9NfMCU,14834
|
|
104
|
-
calkit/tests/test_core.py,sha256=
|
|
104
|
+
calkit/tests/test_core.py,sha256=m-P2OSWv1MjABY2YgSJJdtkvzPNPOspdvcLXT2j0GDs,18106
|
|
105
105
|
calkit/tests/test_dependencies.py,sha256=Stb_pQr8HTrkUSTjZFAJ2aFOXAzlFNXtEjf2Uh0PF2E,12672
|
|
106
106
|
calkit/tests/test_detect.py,sha256=rNi_W5ck4_TZo2_tXfIfTc-U_GxOePdz_jJjaqnewlA,49981
|
|
107
107
|
calkit/tests/test_docker.py,sha256=mBjj_z47fqVLfZTbuJoJwXTAo-udd22RZGR-IXTX9a4,20704
|
|
108
108
|
calkit/tests/test_docs_references.py,sha256=n9XZMniIH1XvlUxdlFcLeQuNBwJsDydyVYZOeAh2XrA,5264
|
|
109
|
-
calkit/tests/test_docx.py,sha256=
|
|
109
|
+
calkit/tests/test_docx.py,sha256=wYTVX2axWkMZC4cxzQsJjllqf__O-6WaAHMB67FHTbg,28481
|
|
110
110
|
calkit/tests/test_environments.py,sha256=sJoarJMpSub1u9FvqY-KcVzdfg7svpgDEUeajC4DcDQ,62874
|
|
111
111
|
calkit/tests/test_fs.py,sha256=vBbzZlikzwAQkPwn8_-wdsD-l1R4YTH7g-ghGfWrtzE,13785
|
|
112
112
|
calkit/tests/test_git.py,sha256=i_m11l5MkgjzuI0hB6PwmSCQ-Vx_Bgk2moZEorpODxo,26016
|
|
113
|
-
calkit/tests/test_hub.py,sha256=
|
|
113
|
+
calkit/tests/test_hub.py,sha256=H7We9KFZnW2Y5SqKVRqQCOksCjZHDoFc_IFJmiXDANQ,17824
|
|
114
114
|
calkit/tests/test_install.py,sha256=ZpM0HAVLoddtyWuuOoATIkJoCpst0w6TIzXIY-ZQik4,14739
|
|
115
115
|
calkit/tests/test_invenio.py,sha256=TJI8HsWkrLl0XOW-K51wdoRd-LH6RhpfvgRonjcdnZI,3693
|
|
116
116
|
calkit/tests/test_julia.py,sha256=q7UYIJAvCQ8b6eMaledcV3rV3PPL6hDnpYJEzn2tmCE,6390
|
|
@@ -145,19 +145,19 @@ calkit/tests/cli/test_import.py,sha256=snQgCw6SM9V3g5NS8oBK-2B3bYFVyJcuXtJuFtDtf
|
|
|
145
145
|
calkit/tests/cli/test_latex.py,sha256=6UTu1vMjv3a7PCXQvB6nLdyRFDyWn-VvlgiOpUIRqhQ,36998
|
|
146
146
|
calkit/tests/cli/test_list.py,sha256=rnoDS2JOiy2TN_zj1LI2IgIa0WKP8UA1zlj4_-uLjpI,13510
|
|
147
147
|
calkit/tests/cli/test_new.py,sha256=WqesquN9fuDs04en5TW06MhVNbwITQBcsi_D_fuTwcQ,76963
|
|
148
|
-
calkit/tests/cli/test_notebooks.py,sha256=
|
|
148
|
+
calkit/tests/cli/test_notebooks.py,sha256=FkD2HD9kveyKuXU55MgHoYyTnOGNzS-FtFIdVtMHzkc,12270
|
|
149
149
|
calkit/tests/cli/test_overleaf.py,sha256=VRBXGmU_8dvIsn3marpV5BmviCq3ofij_FtvqFkW_eQ,49790
|
|
150
150
|
calkit/tests/cli/test_scheduler.py,sha256=YfJi_fcRlQt1bdFyo3b8BFelA1PcH-wn5lAVXiH7Zj4,24109
|
|
151
151
|
calkit/tests/cli/test_show.py,sha256=9vXLdRvHuFvXDRbTgTrMJZS9o_EYr55GyqMfpHHBUi4,3327
|
|
152
152
|
calkit/tests/cli/test_sync.py,sha256=K8FbLq0PPE_JJmYRdXxlWcMx7IAAo-VjiVsfIPj-aYI,4811
|
|
153
|
-
calkit/tests/cli/test_update.py,sha256=
|
|
153
|
+
calkit/tests/cli/test_update.py,sha256=5OL7VjSFxdD4i1pztFIWd_5WDx2oEgh4jTIBBEb27fo,23648
|
|
154
154
|
calkit/tests/cli/main/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
155
155
|
calkit/tests/cli/main/test_core.py,sha256=lyTQcMBmKIqPRiUaVXE5HqGNqomQU4ySSxtpVVD174U,112702
|
|
156
156
|
calkit/tests/cli/main/test_lock.py,sha256=1iX8iSbGtr4O9OkaAi4d3gP3_QzNl6Rr6HLtbEpS6iU,9847
|
|
157
157
|
calkit/tests/cli/main/test_subprojects.py,sha256=2IHwonTH_nI6yraFsgvOQTQ6z6JWOFOmMjZngOeMrwY,18920
|
|
158
158
|
calkit/tests/cli/main/test_xr.py,sha256=0ExuYuub66wsC2tYHnj3lzlgsFsjGjkDyCV2RBIkm4U,28832
|
|
159
159
|
calkit/tests/dvc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
160
|
-
calkit/tests/dvc/test_core.py,sha256=
|
|
160
|
+
calkit/tests/dvc/test_core.py,sha256=ANDdwOO-eKwV_jGxrkOY8CVC8h8EQHt9Loq0JuLP9aE,22043
|
|
161
161
|
calkit/tests/dvc/test_zip.py,sha256=UjluBZYTnw5nisHELz42VRPDk88b0TvDx4QJNVaR-iA,12369
|
|
162
162
|
calkit/tests/jupyterlab/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
163
163
|
calkit/tests/jupyterlab/conftest.py,sha256=Xsh5Zwg0fdqEFPkHDgG7oCHhHJj6WuZLWbmsl7ATABA,207
|
|
@@ -165,28 +165,28 @@ calkit/tests/jupyterlab/test_routes.py,sha256=TEEyoHiKnzzKOkQsJcMPKdqpKFQ25yBCvV
|
|
|
165
165
|
calkit/tests/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
166
166
|
calkit/tests/models/test_core.py,sha256=iDi0mvgZDBj2n7bo-cyKd8dvRZHl5ywWbcab4BPsqcE,8421
|
|
167
167
|
calkit/tests/models/test_iteration.py,sha256=vjuDKwyYQAV4w1qF9VJWQEHksq3KRd6gtT39_a-RhnI,644
|
|
168
|
-
calkit/tests/models/test_pipeline.py,sha256=
|
|
168
|
+
calkit/tests/models/test_pipeline.py,sha256=K-q4LHh35AjvcLxgJnVTMcwsXxIdynCKO16xqdDTtO8,37692
|
|
169
169
|
calkit/agent_skills/add-pipeline-stage/SKILL.md,sha256=kpYYb-rJsS4B9p1Ub3gL21irCG5ZZ4DvWZpdJeEwPZk,3772
|
|
170
170
|
calkit/agent_skills/build-paper-pipeline/SKILL.md,sha256=aV9t86FxQNkeXyswM03fb5LW3ns4pcsWQyYNZ33niLI,6829
|
|
171
171
|
calkit/agent_skills/check-questions/SKILL.md,sha256=yn6geOQUNQnJbM_Bn9grULerzreGvZob8vNtawWgvCs,9653
|
|
172
172
|
calkit/agent_skills/check-reproducibility/SKILL.md,sha256=5SGmtnbq8bSJW0Zwav_aVzfuhilHwBkkdxGuZvPHxGU,4699
|
|
173
173
|
calkit/agent_skills/conventions/SKILL.md,sha256=W9ayIG76Q8BTBgIRhpZz52dCKDLsSVj8_7O3LPeffU4,10323
|
|
174
174
|
calkit/agent_skills/create-pipeline/SKILL.md,sha256=2FGw1iDQVRk5FUq79GP3lPdgvgof2Wi1O9O-abGGtgs,5422
|
|
175
|
-
calkit_python-0.48.
|
|
176
|
-
calkit_python-0.48.
|
|
177
|
-
calkit_python-0.48.
|
|
178
|
-
calkit_python-0.48.
|
|
179
|
-
calkit_python-0.48.
|
|
180
|
-
calkit_python-0.48.
|
|
181
|
-
calkit_python-0.48.
|
|
182
|
-
calkit_python-0.48.
|
|
183
|
-
calkit_python-0.48.
|
|
184
|
-
calkit_python-0.48.
|
|
185
|
-
calkit_python-0.48.
|
|
186
|
-
calkit_python-0.48.
|
|
187
|
-
calkit_python-0.48.
|
|
188
|
-
calkit_python-0.48.
|
|
189
|
-
calkit_python-0.48.
|
|
190
|
-
calkit_python-0.48.
|
|
191
|
-
calkit_python-0.48.
|
|
192
|
-
calkit_python-0.48.
|
|
175
|
+
calkit_python-0.48.1.data/data/etc/jupyter/jupyter_server_config.d/calkit.json,sha256=CWrZP--JDGz8fsvbAlKr_duTL1vDriGT48SL1YCtkY0,81
|
|
176
|
+
calkit_python-0.48.1.data/data/share/jupyter/labextensions/calkit/package.json,sha256=0hnHqhCly59Wg7urFGahc2mSupCWwiPfwKWCYfnbB2E,6202
|
|
177
|
+
calkit_python-0.48.1.data/data/share/jupyter/labextensions/calkit/schemas/calkit/package.json.orig,sha256=egkbkHmJYzDARH3_4qpoF75_Svpj_dfPaIwRU-PNjW4,6064
|
|
178
|
+
calkit_python-0.48.1.data/data/share/jupyter/labextensions/calkit/schemas/calkit/plugin.json,sha256=YSpIrwpwB-lQBk9Mwv-npedWTOOZreO6nlWZhqlWyGI,840
|
|
179
|
+
calkit_python-0.48.1.data/data/share/jupyter/labextensions/calkit/static/506.c34b070098184e33.js,sha256=0PxdmgdZvLeh6RBcfgN1pcriNO8oCeTK5WJvUIEA3MY,112384
|
|
180
|
+
calkit_python-0.48.1.data/data/share/jupyter/labextensions/calkit/static/57d13a7399cec3c5.png,sha256=svHD7-cMtTnRITFwugwsVaB9nZ-h8A61ose8z32HiRE,24850
|
|
181
|
+
calkit_python-0.48.1.data/data/share/jupyter/labextensions/calkit/static/616.528427eb54a4a0c0.js,sha256=d0rHq7cqAjj1GNAMLOpl8dyY8yyKUQcLPCOUr-wJGdQ,8129
|
|
182
|
+
calkit_python-0.48.1.data/data/share/jupyter/labextensions/calkit/static/740.6bf87276e9e5788a.js,sha256=4E0sK8x-ldVkL-1sM07wgJGS1vtVD9Uly7VtoFTi23c,221
|
|
183
|
+
calkit_python-0.48.1.data/data/share/jupyter/labextensions/calkit/static/899.f9b9f9bf705a6493.js,sha256=RNTIItG9k-A_Ip54fRR3QMCjiffkY0bo8zQ2Xh7TyOY,57096
|
|
184
|
+
calkit_python-0.48.1.data/data/share/jupyter/labextensions/calkit/static/935.46ecc6bf99aa593a.js,sha256=Cmp0KWM5YDZ-zcsJWWgqCMIF6adeHQH80rxz1sas45E,51183
|
|
185
|
+
calkit_python-0.48.1.data/data/share/jupyter/labextensions/calkit/static/remoteEntry.109a3a8379365e59.js,sha256=4JavPLcn703qVSC2E1_MD1thrJNL4N-eITjnfNhSDSM,15066
|
|
186
|
+
calkit_python-0.48.1.data/data/share/jupyter/labextensions/calkit/static/style.js,sha256=LTlA3M58hwjqmhomdcrNRO9GM58zZcf0V76WX9HI6EQ,146
|
|
187
|
+
calkit_python-0.48.1.data/data/share/jupyter/labextensions/calkit/static/third-party-licenses.json,sha256=2BGdItLJwO3fAopLl4eJvp26TEwuscYC0-yFaF-LaLU,13683
|
|
188
|
+
calkit_python-0.48.1.dist-info/METADATA,sha256=91q0lRwvRNsd6Pri_L1zXu-N3_DvupV4gKF3RAKc_qE,22044
|
|
189
|
+
calkit_python-0.48.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
190
|
+
calkit_python-0.48.1.dist-info/entry_points.txt,sha256=TtpHpcv2vFVAVYxFVp0XrwCV_z7FWIOguEiypXRDoOw,131
|
|
191
|
+
calkit_python-0.48.1.dist-info/licenses/LICENSE,sha256=9ZamCaSUTZk9rcrnf-sWFKLOHr3ws-S_dgKMegW4nw8,1056
|
|
192
|
+
calkit_python-0.48.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|