Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
9ef20fc
feat: Models the core contract needs
Oct 2, 2026
771d48a
Feat: Protocols and Exceptions added for the core contract
Oct 2, 2026
5fe30c2
Feat: Private protocols for the BatchBackend
Oct 2, 2026
89d0bc9
feat: a fake backend to run unit tests against
Oct 6, 2026
af4c436
fix: some code cleanup in the helper functions
Oct 6, 2026
99f82ae
feat: setup for the unit test suite
Oct 6, 2026
e041acf
chore: style fix
Oct 6, 2026
d287307
chore: styling, unit test fixture
Oct 6, 2026
e830545
chore: single letter var `h` refering to hanlde changed to `handle`
Oct 6, 2026
be1e998
fix: unused parameter in `test_injected_failure_is_reported_per_job()…
Oct 6, 2026
2aeb589
chore: Making room in local fork for my documentation so i can easily…
Oct 7, 2026
5024ce9
fix: placed in the wrong folder :/
Oct 7, 2026
2c32012
chore: added testplan
Oct 7, 2026
3bf4210
feat: Models the core contract needs
Oct 2, 2026
0a6a9e1
Feat: Protocols and Exceptions added for the core contract
Oct 2, 2026
36cf7df
Feat: Private protocols for the BatchBackend
Oct 2, 2026
357050e
feat: a fake backend to run unit tests against
Oct 6, 2026
56bce86
fix: some code cleanup in the helper functions
Oct 6, 2026
5923545
feat: setup for the unit test suite
Oct 6, 2026
1f92acf
chore: style fix
Oct 6, 2026
f7cf50c
chore: styling, unit test fixture
Oct 6, 2026
bab3989
chore: single letter var `h` refering to hanlde changed to `handle`
Oct 6, 2026
eb3c644
fix: unused parameter in `test_injected_failure_is_reported_per_job()…
Oct 6, 2026
f727cbb
Merge branch 'feat/core-contract' of github.com:Alexander-Auer/interc…
Oct 7, 2026
feaf5e7
fix: made the OutputSpec optional as it is not suppoerted by all and …
Oct 7, 2026
46fd1a3
Feat: Core contract issue #16 (#1)
Alexander-Auer Oct 7, 2026
42dce34
fix: These documents should not exist in this way in the main repository
Oct 7, 2026
d67d380
fix: None check for the outputs in submissionspec
Oct 7, 2026
910b0b0
chore: trailing white spaces
Oct 7, 2026
a9fcca0
chore: fixed pre commit locally
Oct 7, 2026
74c40a7
chore: removed pdf, as they just take up space and no one will use th…
Oct 9, 2026
5cb1ee3
fix: JobHandle key is now a combination of the handle's fields instea…
Oct 19, 2026
b6af530
Merge branch 'main' into feat/core-contract
Alexander-Auer Oct 19, 2026
f90cdd4
feat: backend configuration and a closed registry
Alexander-Auer Oct 19, 2026
9dac6f5
test: expand unit test coverage to every logical case
Alexander-Auer Oct 20, 2026
0338aa2
feat: credential models and provider-based supply
Alexander-Auer Oct 20, 2026
ceed1df
Merge branch 'main' into feat/core-contract
Alexander-Auer Oct 23, 2026
d310fe8
test: trim the unit tests to logical cases that can happen
Oct 23, 2026
529fa75
Merge branch 'feat/core-contract' into stack/registry
Oct 23, 2026
5d94d0f
test: trim the registry and configuration tests to logical cases
Oct 23, 2026
60906ef
Merge branch 'stack/registry' into stack/test-expansion
Oct 23, 2026
ae6ca0a
test: trim the expanded tests to logical cases that can happen
Oct 23, 2026
359ece8
Merge branch 'stack/test-expansion' into stack/credential-models
Oct 23, 2026
f7e5eea
test: trim the credential tests to logical cases that can happen
Oct 23, 2026
a644ce6
docs: follow the Diátaxis structure for the user documentation
Oct 23, 2026
b6e172f
fix: address the review of the core contract
Oct 23, 2026
bc5619e
Merge branch 'feat/core-contract' into stack/registry
Oct 23, 2026
7391fbe
fix: no references to the ADRs in the registry and configuration
Oct 23, 2026
9c89415
Merge branch 'stack/registry' into stack/test-expansion
Oct 23, 2026
673cfa4
Merge branch 'stack/test-expansion' into stack/credential-models
Oct 23, 2026
ceb871e
fix: no references to the ADRs in the credential code
Oct 23, 2026
a0211b4
Merge DIRACGrid main into stack/registry (the core contract, squash-m…
Oct 26, 2026
5d10b5a
fix: apply the core contract review to the registry
Oct 26, 2026
7d86596
refactor: rename registry.backend() to get_backend()
Oct 26, 2026
b4fb7bd
refactor!: one public configuration model per backend kind
Oct 26, 2026
03efb2a
docs: generate the configuration reference from the models
Oct 26, 2026
2191feb
Merge branch 'stack/registry' into stack/test-expansion
Oct 26, 2026
929ea61
test: remove leftovers of the flat configuration model
Oct 26, 2026
ce1c811
Merge branch 'stack/test-expansion' into stack/credential-models
Oct 26, 2026
af2109f
fix: Rename FakeSettings to FakeConfig to stay in line with the revie…
Oct 26, 2026
23641f6
Merge remote-tracking branch 'origin/stack/registry' into stack/test-…
Oct 27, 2026
6dca5ef
test: keep the existing model tests where they are
Oct 27, 2026
d7e045f
Merge DIRACGrid main into stack/test-expansion (the registry, squash-…
Oct 27, 2026
b94e917
test: a configuration that is not a mapping is refused
Oct 27, 2026
66af4f2
Merge branch 'stack/test-expansion' into stack/credential-models
Oct 27, 2026
e55693f
Merge DIRACGrid main into stack/credential-models (the test expansion…
Oct 27, 2026
ca8d39f
fix: credential errors never repeat the value they were given
Oct 27, 2026
eccfbc6
fix: a provider's error text stays out of the AuthenticationError
Oct 27, 2026
8e2f76c
docs: document the credential fields in the API reference
Oct 27, 2026
2670cfb
docs: Google-style docstrings for the credential functions
Oct 27, 2026
d5f2216
Merge stack/htcondor-1-core: the shared HTCondor core
Nov 3, 2026
42f1a62
feat: an HTCondor-CE configuration can name its schedd
Nov 3, 2026
25f97c9
fix: a local input that cannot be read is refused on submit
Nov 3, 2026
715810a
test: give each backend its own folder in the unit tests
Nov 3, 2026
815fc7f
feat: the HTCondor-CE backend's building blocks
Nov 3, 2026
082e2f6
feat: HTCondor submit descriptions in the shared core
Nov 3, 2026
3bee45d
feat: HTCondorCEBackend with submit and status
Nov 3, 2026
2f12e30
feat: kill and purge on the HTCondor-CE backend
Nov 3, 2026
8c5128a
feat: HTCondor commands and the state map in the shared core
Nov 3, 2026
296cd9b
test: HTCondor-CE submit and status against simulated tools
Nov 3, 2026
b84ab11
test: purge releases a job, it does not have to forget it
Nov 3, 2026
89c3b94
fix: Removed the x509 proxy as it will not be used anymore, and also …
Nov 4, 2026
e3fc956
Merge branch 'main' into stack/credential-models
Alexander-Auer Nov 4, 2026
be3a762
fix: no X.509 proxy in HTCondor submit descriptions
Nov 4, 2026
34b0061
Merge branch 'stack/htcondor-1-core' into stack/htcondor-3-submit-status
Nov 4, 2026
1ab3f31
Merge remote-tracking branch 'origin/stack/credential-models' into st…
Nov 4, 2026
938b543
fix: the HTCondor-CE backend authenticates with one token
Nov 4, 2026
c85eda0
Merge branch 'stack/htcondor-3-submit-status' into stack/htcondor-4-k…
Nov 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/user/reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ Anything else, including every name starting with `_`, is internal.

::: intercede.registry

## Credentials

::: intercede.auth.models

::: intercede.auth.protocols

::: intercede.auth.providers

## Exceptions

::: intercede.exceptions
11 changes: 11 additions & 0 deletions src/intercede/_backends/_htcondor/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""What interCEde knows about HTCondor, shared by every backend that drives it.

Plain functions and data only, with no dependency on any backend class: the
HTCondor-CE backend uses them directly, and the HTCondor scheduler of the
composed backend (issue #21) will use the same ones over its transport. They
share these functions, never a subtype.

- ``submit``: a submit description built from a ``SubmissionSpec``
- ``commands``: the argv of each HTCondor tool, and parsers for their output
- ``status``: the state mapping from job ads to ``JobStatus``
"""
117 changes: 117 additions & 0 deletions src/intercede/_backends/_htcondor/commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
"""The argv of each HTCondor tool, and parsers for what they print.

``remote`` is how a tool reaches its schedd: ``schedd_options(pool, name)``
for a remote schedd, as an HTCondor-CE is reached, or ``()`` for the schedd
of the host the tool runs on, as the composed backend reaches it.
"""

from __future__ import annotations

import re
from collections.abc import Sequence

from intercede.models import JobID

# Ids per command line, so a status poll of thousands of jobs stays bounded.
CHUNK = 100


def schedd_options(pool: str, name: str) -> list[str]:
"""Reach the schedd ``name`` through the collector ``pool`` (host:port)."""
return ["-pool", pool, "-name", name]


def submit(
description: str, pool: str | None = None, name: str | None = None
) -> list[str]:
"""Submit a description file and spool its inputs to the schedd.

Spooling copies the inputs at submit time, so nothing on the submitting
host is needed afterwards. With ``pool`` and ``name`` the schedd is a
remote one (``-remote`` implies spooling); without, it is the local one.
"""
if (pool is None) != (name is None):
raise ValueError("a remote schedd needs both pool and name")

command = ["condor_submit", "-terse"]

if pool is None or name is None:
command.append("-spool")
else:
command.extend(["-pool", pool, "-remote", name])

command.append(description)
return command


def query(
ids: Sequence[JobID], attributes: Sequence[str], remote: Sequence[str]
) -> list[str]:
"""Read the ads of jobs still in the queue."""
return ["condor_q", *remote, *ids, "-attributes", ",".join(attributes), "-json"]


def history(
ids: Sequence[JobID], attributes: Sequence[str], remote: Sequence[str]
) -> list[str]:
"""Read the ads of jobs that have left the queue."""
return [
"condor_history",
*remote,
*ids,
"-attributes",
",".join(attributes),
"-json",
]


def remove(ids: Sequence[JobID], remote: Sequence[str]) -> list[str]:
"""Remove jobs: cancels active ones, releases finished ones."""
return ["condor_rm", *remote, *ids]


def transfer_data(ids: Sequence[JobID], remote: Sequence[str]) -> list[str]:
"""Fetch the spooled output of jobs into the directory they were submitted from."""
return ["condor_transfer_data", *remote, *ids]


def chunks(ids: Sequence[JobID]) -> list[Sequence[JobID]]:
"""Split ids into groups of at most ``CHUNK``."""
return [ids[start : start + CHUNK] for start in range(0, len(ids), CHUNK)]


_TERSE = re.compile(
r"\s*(?P<cluster>\d+)\.(?P<first>\d+)"
r"\s*-\s*"
r"(?P<last_cluster>\d+)\.(?P<last>\d+)\s*"
)


def parse_submit(raw: str) -> list[JobID]:
"""Parse job IDs from condor_submit -terse output, e.g. '12.0 - 12.3'."""
lines = [line for line in raw.splitlines() if line.strip()]

if len(lines) != 1:
raise ValueError(f"unexpected condor_submit output: {raw.strip()!r}")

match = _TERSE.fullmatch(lines[0])
if match is None:
raise ValueError(f"unexpected condor_submit output: {raw.strip()!r}")

cluster = int(match["cluster"])
first = int(match["first"])
last_cluster = int(match["last_cluster"])
last = int(match["last"])

if cluster != last_cluster or last < first:
raise ValueError(f"unexpected condor_submit output: {raw.strip()!r}")

return [JobID(f"{cluster}.{proc}") for proc in range(first, last + 1)]


_REMOVED = re.compile(r"^Job (\d+\.\d+) marked for removal", re.MULTILINE)


def parse_remove(raw: str) -> set[JobID]:
"""Return the ids ``condor_rm`` reports as removed; any other id was not."""
return {JobID(job) for job in _REMOVED.findall(raw)}
124 changes: 124 additions & 0 deletions src/intercede/_backends/_htcondor/status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"""From HTCondor job ads to ``JobStatus``.

Reading only: the state is taken from what HTCondor records about a job, never
from a policy interCEde sets on it. A finished job reads as FAILED when its
payload exited non-zero or was killed by a signal, from ``ExitCode`` and
``ExitBySignal``.

Ported from DIRAC's ``BatchSystems/Condor.py`` (``STATES_MAP`` and
``getCondorStatus``), with differences: DIRAC puts a payload that fails on
hold to recognise it, where the exit code is read here instead; the states
DIRAC leaves unmapped (6, 7) are mapped; and a removed job reports what it did
before it was removed, so a purged job keeps its result.
"""

from __future__ import annotations

import json
from collections.abc import Mapping, Sequence
from typing import Any

from intercede.models import JobID, JobStatus

# The ad attributes every status query asks for.
STATE_ATTRIBUTES: Sequence[str] = (
"ClusterId",
"ProcId",
"JobStatus",
"LastJobStatus",
"ExitCode",
"ExitBySignal",
"HoldReasonCode",
"HoldReason",
)

# HTCondor's JobStatus codes.
_IDLE, _RUNNING, _REMOVED, _COMPLETED, _HELD, _TRANSFERRING, _SUSPENDED = range(1, 8)

_STATES: Mapping[int, JobStatus] = {
_IDLE: JobStatus.WAITING,
_RUNNING: JobStatus.RUNNING,
_TRANSFERRING: JobStatus.RUNNING, # transferring output after running
_SUSPENDED: JobStatus.WAITING,
}

_HOLD_SPOOLING = 16 # HoldReasonCode: input files are still being spooled


def job_id(ad: Mapping[str, Any]) -> JobID:
"""Return the ``cluster.proc`` id of a job ad."""
return JobID(f"{ad['ClusterId']}.{ad['ProcId']}")


def job_status(ad: Mapping[str, Any]) -> JobStatus:
"""Map one job ad to the common status."""
code = ad.get("JobStatus")
if code == _COMPLETED:
return _result(ad)
if code == _REMOVED:
# purge() removes finished jobs to release them, after which the
# history only says "removed": the state it left tells the result.
return (
_result(ad) if ad.get("LastJobStatus") == _COMPLETED else JobStatus.ABORTED
)
if code == _HELD:
# Held by the site or by HTCondor, not by interCEde: the job is stuck,
# except while its inputs are still being spooled.
return (
JobStatus.WAITING
if ad.get("HoldReasonCode") == _HOLD_SPOOLING
else JobStatus.ABORTED
)
return (
_STATES.get(code, JobStatus.UNKNOWN)
if isinstance(code, int)
else JobStatus.UNKNOWN
)


def is_removed(ad: Mapping[str, Any]) -> bool:
"""Tell whether a job was removed, even while it is still in the queue.

``condor_rm`` only marks a job for removal: the schedd drops it from the
queue a moment later. In between it is already released.

Args:
ad: A job ad with ``JobStatus``.

Returns:
True when the job has been removed.

"""
return ad.get("JobStatus") == _REMOVED


def _result(ad: Mapping[str, Any]) -> JobStatus:
"""Tell DONE from FAILED for a job whose payload has finished."""
if ad.get("ExitBySignal") is True:
return JobStatus.FAILED
exit_code = ad.get("ExitCode")
return (
JobStatus.FAILED
if isinstance(exit_code, int) and exit_code != 0
else JobStatus.DONE
)


def hold_reason(ad: Mapping[str, Any]) -> str | None:
"""Return why a held job is held, if it is."""
reason = ad.get("HoldReason")
return reason if ad.get("JobStatus") == _HELD and isinstance(reason, str) else None


def parse_ads(raw: str) -> list[dict[str, Any]]:
"""Parse ``condor_q -json`` or ``condor_history -json`` output.

``condor_q`` prints nothing at all when no job matches, and
``condor_history`` prints an empty list.
"""
if not raw.strip():
return []
ads = json.loads(raw)
if not isinstance(ads, list) or not all(isinstance(ad, dict) for ad in ads):
raise ValueError("expected a JSON list of job ads")
return ads
Loading
Loading