Cette page a été traduite automatiquement de l’anglais et peut contenir des erreurs. Original en anglais
Code source de axqua.jobs.ids
"""Job identifiers: readable, sortable, collision-safe.
A bare UUID was rejected deliberately. The identifier is a **directory name** and a
**dashboard column**, and ``f835ac7d-3e91-...`` is neither greppable nor meaningful six
months later - the dashboard sketch in the plan truncates it to ``f835ac7``, which is the
problem showing through. So::
<date>-<case-slug>-<kind-slug>-<6 hex>
2026-08-14-isar-2025-steady-a3f19c
The date sorts chronologically under a plain ``ls``, the case and kind say what the job
is without opening anything, and the random tail keeps it collision-safe. The tail is
short on purpose: uniqueness is *claimed* by creating the directory with
``mkdir(exist_ok=False)`` (see :mod:`axqua.jobs.paths`), not asserted by entropy, so
six hex digits plus a redraw loop is ample.
Standard library only - this module is imported by the CLI on every job verb.
"""
from __future__ import annotations
import datetime as _dt
import re
import secrets
from axqua.core.errors import ConfigError
#: The one grammar. Anything that does not match is not a job id, which is what lets
#: ``axqua status <arg>`` tell a job id from a case-config path without guessing.
JOB_ID_RE = re.compile(
r"^(?P<date>\d{4}-\d{2}-\d{2})"
r"-(?P<slug>[a-z0-9][a-z0-9-]{0,60}?)"
r"-(?P<tail>[0-9a-f]{6})$"
)
_SLUG_STRIP = re.compile(r"[^a-z0-9]+")
_SLUG_EDGES = re.compile(r"^-+|-+$")
#: How many times :func:`make_job_id` redraws the random tail before giving up. The
#: caller claims the directory; a clash means it lost that race, which at six hex digits
#: needs a deliberate effort to arrange.
MAX_REDRAWS = 8
[docs]
def slug(text: str, *, limit: int = 32) -> str:
"""Reduce *text* to a lowercase ``[a-z0-9-]`` token fit for a directory name.
Empty or wholly non-alphanumeric input becomes ``"case"`` rather than an empty
segment, because an empty segment would make the id ambiguous to parse.
"""
out = _SLUG_STRIP.sub("-", str(text).lower())
out = _SLUG_EDGES.sub("", out)[:limit]
out = _SLUG_EDGES.sub("", out)
return out or "case"
[docs]
def make_job_id(case_name: str, kind_slug: str = "", *, when: _dt.date | None = None,
tail: str | None = None) -> str:
"""Build one job id. *tail* is for tests; production draws it randomly."""
day = (when or _dt.date.today()).isoformat()
parts = [day, slug(case_name)]
if kind_slug:
parts.append(slug(kind_slug, limit=16))
parts.append(tail if tail is not None else secrets.token_hex(3))
return "-".join(parts)
[docs]
def parse_job_id(job_id: str) -> tuple[_dt.date, str, str]:
"""Split *job_id* into ``(date, slug, tail)``.
The slug is returned whole - the case and kind halves are not separable after the
fact, since a case name may itself contain dashes. Nothing needs them apart: the
authoritative kind lives in ``job.json``.
"""
match = JOB_ID_RE.match(str(job_id))
if match is None:
raise ConfigError(
f"{job_id!r} is not a job id",
subject="job_id",
remedy="Job ids look like 2026-08-14-isar-2025-steady-a3f19c; "
"run 'axqua list' to see the known ones.",
)
return (
_dt.date.fromisoformat(match.group("date")),
match.group("slug"),
match.group("tail"),
)
[docs]
def is_job_id(value: object) -> bool:
"""True when *value* is spelled like a job id. Never raises."""
return bool(JOB_ID_RE.match(str(value)))