Hands: upload from the headset, then plug in and leave it

Export and upload are done in the headset now: the export page only notes
that VR may stutter a little. Upload opens the pull request first (a
draft) and shows its link, telling the person to plug in the headset and
leave it until it says Uploaded; the files then go to refs/pr/N, and the
pull request is marked open at the end. A retry of the same export goes on
in the same pull request. While an export or upload runs, a host unit holds
a logind sleep inhibitor so the Frame stays awake with the headset off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DeeJanuzandClaude Opus 5.5 committed 2026-10-03 09:20:00 -06:00
1 parent 30e02e9db7
commit af2ea7c0ee
6 files changed
+203 -34

No files matched your search

+6 -4
View File
@@ -312,7 +312,7 @@ On 2026-10-02 a whole session showed "I can't see your hands": after the headset
- The side cameras are named right in every exported set when the session's `sides.swapped` is known: parts that need it get slam_left and slam_right (and their `_dk`) exchanged in the set headers as they're compressed. The exported take.json says `"parts": {"sets.bin": {"names_swapped": <swapped>}}`, and the manifest's take entry `"sides": {"names_swapped", "renamed_sets"}`. Unknown, the names stay as recorded.
- `SHA256SUMS`.
Compression runs at nice 19. Before starting, the window warns if the headset is worn, judged the way `frame-job` does: `vrcompositor` runs and a `/sys/class/backlight/*/brightness` reads over 0 (SteamVR turns the panel off 5 s after the headset comes off). CPU work while in VR causes stutter. The proximity sensor is no use here: it read 9-43 with the headset sitting unworn.
Compression runs at nice 19. While it runs with the headset worn, the window notes that VR may stutter a little (exports are done in the headset). Worn is judged the way `frame-job` does: `vrcompositor` runs and a `/sys/class/backlight/*/brightness` reads over 0 (SteamVR turns the panel off 5 s after the headset comes off). CPU work while in VR causes stutter. The proximity sensor is no use here: it read 9-43 with the headset sitting unworn.
- **Upload.** The Upload page uploads an export from the window. It also keeps the manual way as a fallback: `UPLOAD.md` with the export's path and size filled in, plus the copyable command (`validate.py`, then `hf upload ... --create-pr`).
- **`hands/rec/validate.py`** (standard library) checks an export. The window runs it before an upload, and the maintainer runs it on each submission (`validate.py DIR [--json]`, exit status 1 on errors). It checks:
- `SHA256SUMS`: every file listed and matching.
@@ -335,11 +335,13 @@ On 2026-10-02 a whole session showed "I can't see your hands": after the headset
3. Stop while the texts are drafts, unless `FT_HANDREC_ALLOW_UPLOAD=1`.
4. `whoami`: a read-only token is refused.
5. `auth_check` on the dataset: a gated dataset whose terms aren't accepted gives "accept the dataset's terms first", with the link.
6. `upload_folder(repo_id=HF_DATASET, repo_type="dataset", folder_path=EXPORT, path_in_repo="contributions/<contributor>/<session>", create_pr=True, commit_message=..., commit_description=...)`. The description summarizes the manifest: takes, minutes, sets, lighting, objects, controllers, the consent and tool versions, the size, and validate's warnings.
7. Add `{"repo", "pr_url", "uploaded", "export_sha"}` to `uploads` in `session.json`. `export_sha` is the SHA256 of `SHA256SUMS`. The file keeps its modification time, so the export doesn't count as out of date.
6. Open the pull request first: `create_pull_request(HF_DATASET, title, description=...)`, an empty draft. Its link goes to the window right away (`{"phase": "opened", "pr_url"}`), which tells the person to plug the headset in and leave it. Record `{"repo", "pr_url", "pr_num", "started", "export_sha", "status": "started"}` in `uploads` in `session.json`. A retry of the same export goes on in that pull request while it's draft or open.
7. `upload_folder(repo_id=HF_DATASET, repo_type="dataset", folder_path=EXPORT, path_in_repo="contributions/<contributor>/<session>", revision="refs/pr/N", commit_message=..., commit_description=...)`. The description summarizes the manifest: takes, minutes, sets, lighting, objects, controllers, the consent and tool versions, the size, and validate's warnings. Then mark the pull request open if it's still a draft (the maintainer's `list` shows open ones, so drafts are uploads still in progress).
8. Mark the record `"status": "done"` with `uploaded`. `export_sha` is the SHA256 of `SHA256SUMS`. The file keeps its modification time, so the export doesn't count as out of date. Only finished records count as "uploaded before" (records without a status are from before 2026-10-03 and finished).
Errors get a plain explanation: not logged in, a token Hugging Face rejects (401), a token that can't open a pull request (403), terms not accepted, dataset not found, network errors. `--dry-run` does everything except the network calls and the record, and lists what it would upload.
- **The page** shows the login (`whoami`, with "Check again"). If nobody is logged in, it explains how to run `distrobox enter dev -- hf auth login` in Konsole with a write token: the token goes only into that terminal. The page then has Upload and Cancel, the phase with a progress bar (a share while the export is checked, a sweep while it's sent, as `huggingface_hub` reports no progress), and the pull request's link when it's done. If this export was uploaded before, the page says so, and uploading it again asks first. A stale export can't be uploaded.
- **The page** shows the login (`whoami`, with "Check again"). If nobody is logged in, it explains how to run `distrobox enter dev -- hf auth login` in Konsole with a write token: the token goes only into that terminal. The page then has Upload and Cancel, the phase with a progress bar (a share while the export is checked, a sweep while it's sent, as `huggingface_hub` reports no progress), the pull request's link once it's open, with "plug in the headset and leave it plugged in until this says Uploaded", and Uploaded at the end. If this export was uploaded before, the page says so, and uploading it again asks first. A stale export can't be uploaded.
- **Staying awake:** while an export or an upload runs, the window holds a host unit, `frametop-handrec-awake.service`, running `systemd-inhibit --what=sleep:idle --mode=block ... sleep infinity`, and stops it when the last of them ends (or on quit). It's meant to keep Steam from putting the Frame to sleep with the headset off; whether Steam's sleep honours a logind block inhibitor is still to be checked on the device. Exports and uploads are done in the headset: the export page only notes that VR may stutter a little while it compresses.
- **While the texts are drafts**, Upload stays off unless `FT_HANDREC_ALLOW_UPLOAD=1`, so the maintainer can rehearse against a private test repo. `FT_HANDREC_DATASET` overrides `HF_DATASET`. `ft-handrec --hub-dry-run` makes Upload a dry run: no network, so it isn't held back by the drafts.
- **Rehearsal: `hands/rec/rehearse.sh [--repo ID]`** runs it all without the headset, in the dev container, in one `frame-job --local` scope when frame-job is installed. `ft-ringplay` plays 30 s of a recording into a ring in `/run/user/UID`. A tracking ft-hands that's already running is used, or one is started on that ring. `ft-handpanel --no-vr` stands in for the panel. `session.py --no-start --next-after 0.3` records a three-section test script (a prompt, a two-cue sweep, no hands) in step mode, three parts of 6 s (countdown and hold), about 360 MB once exported. Then `takes.py` exports, `validate.py` checks, and `hub.py` uploads: a dry run by default, or for real to `--repo ID` with `FT_HANDREC_ALLOW_UPLOAD=1`. It prints a summary, deletes its temporary folders (camera images of a room) and stops everything it started, Ctrl+C included. The `--no-vr` panel logs no poses, so `poses.jsonl` is missing there (a warning).
+5 -3
View File
@@ -10,6 +10,8 @@ Your export is ready:
Uploads go to the Hugging Face dataset [@DATASET@](https://huggingface.co/datasets/@DATASET@), from your own Hugging Face account. Nothing is uploaded until you press Upload (or run the command below). The upload opens a pull request, so nothing is published until the maintainer has looked at it.
You can do every step below in the headset: the web pages in a browser window, the login in a terminal window. Once your pull request is open, you plug the headset in and leave it to finish.
## 1. Make a Hugging Face account
Sign up at <https://huggingface.co/join>, if you don't have an account. Your username shows on your pull request, but the dataset credits your contributor id, not your name.
@@ -38,11 +40,11 @@ If the page says `huggingface_hub` isn't installed, update the dev container fir
## 5. Upload
Press **Upload** on this page. It first checks the export (that every file is complete and matches its checksum, and that nothing identifying is left in), then uploads it to `contributions/@CONTRIBUTOR@/@SESSION@` in the dataset and opens a pull request. Large uploads take a while, and Cancel stops it. When it's done, the page shows your pull request's link.
Press **Upload** on this page. It first checks the export (that every file is complete and matches its checksum, and that nothing identifying is left in). Then it opens your pull request and shows its link, and starts uploading the files to it, into `contributions/@CONTRIBUTOR@/@SESSION@` in the dataset.
If it fails partway, press Upload again: files already sent usually aren't sent twice. If a pull request was opened anyway, that's fine: the maintainer closes the incomplete one.
**Once the link shows, plug in the headset and leave it plugged in until the page says Uploaded.** A round is several gigabytes, so this can take a while. You can take the headset off: the Hand Recorder keeps it awake until the upload is done. Keep the Hand Recorder open, since closing it stops the upload.
Keep the headset on its charger or plugged in while it uploads.
If it stops partway (Cancel, or the network drops), press Upload again: it carries on in the same pull request, and files already sent aren't sent twice.
### Or upload from a terminal
+41 -3
View File
@@ -57,6 +57,7 @@ UPLOAD_PATH = hub.UPLOAD_PATH
HUB_PATH = os.path.join(HERE, "hub.py")
VALIDATE_PATH = os.path.join(HERE, "validate.py")
# What `hf auth login` needs: run in a terminal, where the token is typed (never in this window).
AWAKE_UNIT = "frametop-handrec-awake.service"
LOGIN_COMMAND = "distrobox enter dev -- hf auth login"
SCRIPT_PATH = os.path.join(HERE, "script.json")
# The headset counts as worn while vrcompositor runs and a display panel is lit: SteamVR turns
@@ -243,6 +244,8 @@ class Backend(QObject):
self._lighting_measured = ""
self._lighting_busy = False
self._camd_started = False # ft-camd started for the light check: stopped on quit
self._awake = set() # what keeps the Frame awake now: "export", "upload"
self._awake_lock = threading.Lock()
self._export_cancel = None
self._export_thread = None
self._export_fraction = 0.0
@@ -644,6 +647,11 @@ class Backend(QObject):
if self._upload_proc:
self.cancelUpload()
self._upload_thread.join(10)
if self._awake and self._session_mod:
try:
self._session_mod.stop_unit(AWAKE_UNIT)
except Exception:
pass
if self._camd_started and self._session_mod:
try:
self._session_mod.stop_unit(self._session_mod.CAMD_UNIT)
@@ -780,6 +788,30 @@ class Backend(QObject):
return self._export_session
@Slot(str, bool)
def _stay_awake(self, why, on):
"""Hold off sleep while an export or upload runs, so the headset can be taken off and
left plugged in: a host unit running systemd-inhibit, started with the first reason
and stopped with the last."""
mod = self._runner()
if not mod or self._hub_dry_run:
return
def run():
with self._awake_lock:
had = bool(self._awake)
(self._awake.add if on else self._awake.discard)(why)
try:
if self._awake and not had:
mod.start_unit(AWAKE_UNIT, "keeps the Frame awake while exporting or uploading",
["systemd-inhibit", "--what=sleep:idle", "--mode=block",
"--who=Frametop Hand Recorder", "--why=Exporting or uploading hand recordings",
"sleep", "infinity"])
elif had and not self._awake:
mod.stop_unit(AWAKE_UNIT)
except Exception as e:
print(f"keeping the Frame awake: {e}", file=sys.stderr, flush=True)
self._thread(run)
def exportSession(self, session, keep_notes):
if self.exporting or self._active_guard(session):
return
@@ -789,6 +821,7 @@ class Backend(QObject):
self._export_fraction = 0.0
self._export_text = "Starting"
self.exportChanged.emit()
self._stay_awake("export", True)
def run():
try:
@@ -815,6 +848,7 @@ class Backend(QObject):
def _on_export_finished(self, path, error):
self._export_cancel = None
self._stay_awake("export", False)
if error == "cancelled":
self._export_text = "Cancelled: nothing was kept"
elif error:
@@ -968,8 +1002,9 @@ class Backend(QObject):
self._upload_proc = proc
self._upload_cancelled = False
self._upload = {"session": session, "phase": "check", "text": "Starting", "fraction": 0.0, "log": "",
"result": {}, "error": {}}
"result": {}, "error": {}, "pr_url": ""}
self.uploadChanged.emit()
self._stay_awake("upload", True)
def run():
last = {}
@@ -1009,11 +1044,14 @@ class Backend(QObject):
if "phase" in obj:
f = obj.get("fraction")
up.update(phase=obj["phase"], text=obj.get("text", ""), fraction=-1.0 if f is None else float(f))
if obj.get("pr_url"):
up["pr_url"] = obj["pr_url"] # the pull request is open: the files are on their way
self._upload = up
self.uploadChanged.emit()
def _on_upload_finished(self, last):
self._upload_proc = None
self._stay_awake("upload", False)
up = dict(self._upload)
if "done" in last:
result = last["done"]
@@ -1025,8 +1063,8 @@ class Backend(QObject):
e = last.get("error") or {}
cancelled = e.get("kind") == "cancelled"
up.update(error=e, result={}, phase="failed", fraction=0.0,
text=("Cancelled. If it had got as far as opening the pull request, check the dataset's "
"Community tab: an incomplete one can be closed there.") if cancelled else e.get("text", ""))
text=("Cancelled. Its pull request stays open: press Upload again to finish it there."
if up.get("pr_url") else "Cancelled") if cancelled else e.get("text", ""))
if not cancelled:
self.message.emit("Upload failed", True)
self._upload = up
+66 -19
View File
@@ -12,9 +12,12 @@ An upload:
3. stops while CONSENT.md or UPLOAD.md is a draft, unless FT_HANDREC_ALLOW_UPLOAD=1;
4. checks the login (whoami: a read-only token can't open a pull request) and access to the
dataset (auth_check: a gated dataset's terms must be accepted first);
5. upload_folder(..., create_pr=True) to contributions/<contributor>/<session>;
6. records {"repo", "pr_url", "uploaded", "export_sha"} under "uploads" in session.json.
--dry-run does all of it except the network calls (4 and 5) and recording (6), and says what
5. opens the pull request first (a draft, empty), so its link can be shown while the files go,
and records it under "uploads" in session.json with "status": "started";
6. upload_folder(..., revision="refs/pr/N") to contributions/<contributor>/<session>; a retry of
the same export goes on in the same pull request while it's still open;
7. marks the record {"repo", "pr_url", "pr_num", "uploaded", "export_sha", "status": "done"}.
--dry-run does all of it except the network calls (4 to 6) and recording (5, 7), and says what
it would upload.
The dataset is HF_DATASET; FT_HANDREC_DATASET overrides it (a test repo, for rehearsals).
@@ -22,7 +25,8 @@ The dataset is HF_DATASET; FT_HANDREC_DATASET overrides it (a test repo, for reh
usage: hub.py [--base DIR] whoami [--json]
hub.py [--base DIR] upload SESSION [--dry-run] [--again] [--json]
With --json, each line of output is one JSON object: {"phase", "text", "fraction"} as it goes,
then {"done": {...}} or {"error": {"kind", "text", "link", "errors"}}. Exit status 0: done.
with {"phase": "opened", "pr_url"} once the pull request exists, then {"done": {...}} or
{"error": {"kind", "text", "link", "errors"}}. Exit status 0: done.
"""
import argparse
import datetime
@@ -114,8 +118,18 @@ def uploads(store, session):
def previous_upload(store, session, sha):
"""The latest upload of this same export, or None."""
same = [u for u in uploads(store, session) if sha and u.get("export_sha") == sha]
"""The latest finished upload of this same export, or None. (Records from before
2026-10-03 have no status: they were written only when an upload finished.)"""
same = [u for u in uploads(store, session) if sha and u.get("export_sha") == sha
and u.get("status", "done") == "done"]
return same[-1] if same else None
def unfinished_upload(store, session, sha, repo):
"""The latest upload of this same export to repo that opened its pull request and didn't
finish, or None."""
same = [u for u in uploads(store, session) if sha and u.get("export_sha") == sha and u.get("repo") == repo
and u.get("status") == "started" and u.get("pr_num")]
return same[-1] if same else None
@@ -129,7 +143,13 @@ def record_upload(store, session, record):
except OSError:
st = None
meta = takes.read_json(path)
meta.setdefault("uploads", []).append(record)
records = meta.setdefault("uploads", [])
same = [i for i, u in enumerate(records) if isinstance(u, dict) and record.get("pr_num")
and u.get("pr_num") == record["pr_num"] and u.get("repo") == record.get("repo")]
if same:
records[same[-1]] = record # the same pull request: started, then done
else:
records.append(record)
takes.write_json(path, meta)
if st:
os.utime(path, ns=(st.st_atime_ns, st.st_mtime_ns))
@@ -228,7 +248,7 @@ def whoami():
def upload(store, session, dry_run=False, again=False, progress=None, log=None):
"""Upload exports/<session> (the steps in this file's docstring). progress(phase, text,
fraction or None); log(text) for the dry run's account. Returns the result; raises HubError."""
tell = progress or (lambda phase, text, fraction=None: None)
tell = progress or (lambda phase, text, fraction=None, **extra: None)
say = log or (lambda text: None)
import validate
repo = dataset_id()
@@ -271,7 +291,7 @@ def upload(store, session, dry_run=False, again=False, progress=None, log=None):
say(f" {rel} {takes.human_bytes(os.path.getsize(os.path.join(export, rel)))}")
say(f"dry run: commit message: {message}")
say("dry run: commit description:\n" + description.rstrip())
record = {"repo": repo, "pr_url": "", "uploaded": now_iso(), "export_sha": sha}
record = {"repo": repo, "pr_url": "", "uploaded": now_iso(), "export_sha": sha, "status": "done"}
say(f"dry run: would record in session.json's uploads: {json.dumps(record)}")
tell("done", "Dry run: nothing was uploaded", 1.0)
return dict(plan, dry_run=True, pr_url="", record=record)
@@ -293,18 +313,43 @@ def upload(store, session, dry_run=False, again=False, progress=None, log=None):
api.auth_check(repo, repo_type="dataset")
except Exception as e:
raise explain(e, repo) from None
tell("upload", f"Uploading {len(files)} files ({takes.human_bytes(report.bytes)}) and opening a pull request",
None)
# The pull request first, so its link shows while the files go (and the person can plug the
# headset in and leave it). A retry of this export goes on in its pull request if it's open.
tell("open", "Opening your pull request", None)
pr = None
before = unfinished_upload(store, session, sha, repo)
if before:
try:
d = api.get_discussion_details(repo, int(before["pr_num"]), repo_type="dataset")
if d.is_pull_request and d.status in ("draft", "open"):
pr = d
except Exception:
pr = None
if pr is None:
try:
pr = api.create_pull_request(repo, message, description=description, repo_type="dataset")
except Exception as e:
raise explain(e, repo) from None
pr_url = getattr(pr, "url", "") or f"{dataset_url(repo)}/discussions/{pr.num}"
record = {"repo": repo, "pr_url": pr_url, "pr_num": pr.num, "started": now_iso(), "export_sha": sha,
"status": "started"}
record_upload(store, session, record)
tell("opened", f"Pull request #{pr.num} is open. Uploading {len(files)} files ({takes.human_bytes(report.bytes)})",
None, pr_url=pr_url)
try:
info = api.upload_folder(repo_id=repo, repo_type="dataset", folder_path=export, path_in_repo=path_in_repo,
create_pr=True, commit_message=message, commit_description=description)
api.upload_folder(repo_id=repo, repo_type="dataset", folder_path=export, path_in_repo=path_in_repo,
revision=f"refs/pr/{pr.num}", commit_message=message, commit_description=description)
except Exception as e:
raise explain(e, repo) from None
pr_url = getattr(info, "pr_url", None) or ""
record = {"repo": repo, "pr_url": pr_url, "uploaded": now_iso(), "export_sha": sha}
try: # a pull request opened through the API stays a draft until it's marked open
if api.get_discussion_details(repo, pr.num, repo_type="dataset").status == "draft":
api.change_discussion_status(repo, pr.num, "open", repo_type="dataset")
except Exception:
pass # the maintainer can open it
record = dict(record, uploaded=now_iso(), status="done")
record_upload(store, session, record)
tell("done", "Uploaded", 1.0)
return dict(plan, dry_run=False, pr_url=pr_url, user=who.get("name", ""), record=record)
return dict(plan, dry_run=False, pr_url=pr_url, pr_num=pr.num, user=who.get("name", ""), record=record)
# ---------------------------------------------------------------- the command line
@@ -334,15 +379,17 @@ def main():
print(f"logged in as {who['name']} (token role: {who['role'] or 'unknown'})")
return 0
if a.json:
def progress(phase, text, fraction=None):
emit({"phase": phase, "text": text, "fraction": fraction})
def progress(phase, text, fraction=None, **extra):
emit(dict(extra, phase=phase, text=text, fraction=fraction))
def log(text):
emit({"log": text})
else:
last = {}
def progress(phase, text, fraction=None):
def progress(phase, text, fraction=None, **extra):
if extra.get("pr_url"):
print(f"pull request: {extra['pr_url']}", flush=True)
line = text if fraction is None else f"{fraction * 100:5.1f}% {text}"
if (phase, text) != last.get("key") or fraction in (0.0, 1.0):
print(line, file=sys.stderr, flush=True)
+29 -5
View File
@@ -1353,11 +1353,11 @@ Kirigami.ApplicationWindow {
DraftBanner { Layout.fillWidth: true }
Kirigami.InlineMessage {
Layout.fillWidth: true
visible: exportView.worn
visible: exportView.worn && backend.exporting
position: Kirigami.InlineMessage.Position.Header
type: Kirigami.MessageType.Warning
text: "Someone seems to be wearing the headset. Exporting keeps the processor busy for a "
+ "while, which can make VR stutter: export when the headset is off, or go ahead anyway."
type: Kirigami.MessageType.Information
text: "Exporting keeps the processor busy for a few minutes, so VR may stutter a little until "
+ "it's done. You can keep using the headset meanwhile."
}
Kirigami.InlineMessage {
Layout.fillWidth: true
@@ -1455,7 +1455,8 @@ Kirigami.ApplicationWindow {
opacity: 0.7
text: "Export leaves out the ranges you deleted, compresses the rest and adds a manifest and "
+ "checksums, in " + backend.exportsDir + ". It runs at the lowest priority "
+ "and takes a few minutes per round. Nothing is uploaded until you press Upload on the Upload page."
+ "and takes a few minutes per round; the headset stays awake until it's done. Nothing is "
+ "uploaded until you press Upload on the Upload page."
}
}
}
@@ -1694,6 +1695,29 @@ Kirigami.ApplicationWindow {
text: uploadView.up.text || ""
}
// The pull request is open and the files are on their way: time to plug in.
Kirigami.InlineMessage {
Layout.fillWidth: true
visible: backend.uploading && uploadView.mine && (uploadView.up.pr_url || "") !== ""
type: Kirigami.MessageType.Positive
text: "Your pull request is open: " + (uploadView.up.pr_url || "") + ". The files are uploading "
+ "to it now, which can take a while. Plug in the headset and leave it plugged in until "
+ "this page says Uploaded. You can take the headset off: it stays awake until the upload "
+ "is done. Keep the Hand Recorder open."
actions: [openOpenedPr, copyOpenedPr]
Kirigami.Action {
id: openOpenedPr
text: "Open"
icon.name: "internet-services"
onTriggered: Qt.openUrlExternally(uploadView.up.pr_url)
}
Kirigami.Action {
id: copyOpenedPr
text: "Copy link"
icon.name: "edit-copy"
onTriggered: backend.copy(uploadView.up.pr_url)
}
}
Kirigami.InlineMessage {
Layout.fillWidth: true
visible: !backend.uploading && uploadView.mine && uploadView.up.phase === "done"
+56
View File
@@ -384,6 +384,62 @@ class HubTest(unittest.TestCase):
with open(os.path.join(export, "manifest.json")) as f:
self.assertNotIn("uploads", json.load(f)["session"])
def test_pull_request_first(self):
"""The pull request opens before the files go; a retry goes on in it."""
calls = []
class Pr:
def __init__(self, num, status="draft"):
self.num, self.status, self.is_pull_request = num, status, True
self.url = f"https://huggingface.co/datasets/a/b/discussions/{num}"
class Api:
fail = True
status = "draft"
def whoami(self):
return {"name": "someone", "auth": {"accessToken": {"role": "write"}}}
def auth_check(self, repo, repo_type=None):
pass
def create_pull_request(self, repo, title, description=None, repo_type=None):
calls.append("create")
return Pr(7)
def get_discussion_details(self, repo, num, repo_type=None):
return Pr(num, Api.status)
def upload_folder(self, **kw):
calls.append(("upload", kw["revision"], kw.get("create_pr")))
if Api.fail:
raise ConnectionResetError("reset")
def change_discussion_status(self, repo, num, status, repo_type=None):
calls.append(("status", num, status))
fake = mock.Mock(HfApi=Api)
seen = []
env = {hub.DATASET_ENV: "a/b", hub.ALLOW_ENV: "1"}
with mock.patch.dict(os.environ, env), mock.patch.object(hub, "_hf", return_value=fake):
with self.assertRaises(hub.HubError):
hub.upload(self.store, SESSION, progress=lambda phase, text, fraction=None, **extra: seen.append(
(phase, extra.get("pr_url"))))
self.assertIn(("opened", "https://huggingface.co/datasets/a/b/discussions/7"), seen)
[rec] = hub.uploads(self.store, SESSION)
self.assertEqual((rec["status"], rec["pr_num"]), ("started", 7))
self.assertIsNone(hub.previous_upload(self.store, SESSION, rec["export_sha"])) # not a duplicate
Api.fail = False
result = hub.upload(self.store, SESSION)
self.assertEqual(calls, ["create", ("upload", "refs/pr/7", None), ("upload", "refs/pr/7", None),
("status", 7, "open")]) # the retry reused #7
self.assertEqual(result["pr_url"], "https://huggingface.co/datasets/a/b/discussions/7")
[rec] = hub.uploads(self.store, SESSION)
self.assertEqual(rec["status"], "done")
with mock.patch.dict(os.environ, env), self.assertRaises(hub.HubError) as cm:
hub.upload(self.store, SESSION, dry_run=True)
self.assertEqual(cm.exception.kind, "duplicate")
def test_explain(self):
try:
import httpx