Skip to content

API Reference: CrossClient

Source code in src/crosscontract/crossclient/crossclient.py
class CrossClient:
    def __init__(
        self,
        username: str,
        password: str,
        base_url: str = DEFAULT_URL,
        verify: bool = True,
    ) -> None:
        """Initialize the client with authentication.

        Args:
            username (str): The username for authentication.
            password (str): The password for authentication.
            base_url (str): If provided, use this domain instead of the default
                DEFAULT_URL.
                The domain must include the protocol (e.g., http:// or https://).
                Example: "http://example.com".
                Defaults to DEFAULT_URL: "https://backend.sweet-cross.ch".
                Trailing slashes are stripped internally.
            verify (bool): Whether to verify SSL certificates.
                Defaults to True.

        Returns:
            CrossClient: An instance of the authenticated client.
        """
        self._base_url = base_url.rstrip("/")  # Ensure no trailing slash
        self._username = username
        self._password = password
        self._verify = verify
        self._token = None

        # Create the client
        timeout = httpx.Timeout(10.0, connect=30.0, read=60.0, write=None)
        limits = httpx.Limits(max_connections=5, max_keepalive_connections=5)
        self._client = httpx.Client(
            base_url=self._base_url,
            verify=verify,
            timeout=timeout,
            limits=limits,
        )
        self._is_closed = False

        # ---- include services ----
        self.contracts: ContractService = ContractService(client=self)
        self.submissions: SubmissionContractService = SubmissionContractService(
            client=self
        )

        # authenticate upon initialization
        self.authenticate()

        # Register cleanup on interpreter shutdown
        atexit.register(self.close)

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.close()

    def __del__(self):
        # Best-effort cleanup during garbage collection
        try:
            self.close()
        except Exception:
            pass

    def __repr__(self):
        return f"CrossClient(base_url={self._base_url}, username={self._username})"

    def close(self):
        """Close the HTTPX client."""
        self._client.close()
        self._is_closed = True

    def authenticate(self) -> str:
        """Authenticate with the server and retrieve an access token.

        Returns:
            str: The authentication token.
        """
        response = self._client.post(
            "/user/auth/login",
            data={"username": self._username, "password": self._password},
        )
        response.raise_for_status()  # Raise an error for bad responses
        token = response.json().get("access_token", "")
        self._token = token
        self._client.headers["Authorization"] = f"Bearer {self._token}"
        return token

    def request(self, method: str, endpoint: str, **kwargs: Any) -> httpx.Response:
        """Send an HTTP request to the specified endpoint.

        Args:
            method (str): The HTTP method (e.g., 'GET', 'POST').
            endpoint (str): The API endpoint to send the request to.
            **kwargs: Additional arguments to pass to the request.

        Returns:
            httpx.Response: The response from the server.
        """
        if self._is_closed:
            raise RuntimeError(
                "Attempted to make a request with a closed CrossClient. Ensure you "
                "are performing all operations within the 'with' context block."
            )
        if not self._token:
            self.authenticate()
        response = self._client.request(method, endpoint, **kwargs)

        # try to get a new token if unauthorized
        if response.status_code == 401:
            # Token expired: Refresh and retry
            self.authenticate()

            # Re-issue the request with the new header (handled by self._client update)
            # We must recreate the request to pick up the new headers from the
            # client state
            response = self._client.request(method, endpoint, **kwargs)
        return response

    def post(self, endpoint: str, json: dict | None = None, **kwargs) -> httpx.Response:
        """Send a POST request to the specified endpoint."""
        return self.request("POST", endpoint, json=json, **kwargs)  # pragma: no cover

    def delete(self, endpoint: str, **kwargs) -> httpx.Response:
        """Send a DELETE request to the specified endpoint."""
        return self.request("DELETE", endpoint, **kwargs)  # pragma: no cover

    def get(self, endpoint: str, **kwargs) -> httpx.Response:
        """Send a GET request to the specified endpoint."""
        return self.request("GET", endpoint, **kwargs)  # pragma: no cover

    def patch(
        self, endpoint: str, json: dict | None = None, **kwargs
    ) -> httpx.Response:
        """Send a PATCH request to the specified endpoint."""
        return self.request("PATCH", endpoint, json=json, **kwargs)  # pragma: no cover

__init__(username, password, base_url=DEFAULT_URL, verify=True)

Initialize the client with authentication.

Parameters:

Name Type Description Default
username str

The username for authentication.

required
password str

The password for authentication.

required
base_url str

If provided, use this domain instead of the default DEFAULT_URL. The domain must include the protocol (e.g., http:// or https://). Example: "http://example.com". Defaults to DEFAULT_URL: "https://backend.sweet-cross.ch". Trailing slashes are stripped internally.

DEFAULT_URL
verify bool

Whether to verify SSL certificates. Defaults to True.

True

Returns:

Name Type Description
CrossClient None

An instance of the authenticated client.

Source code in src/crosscontract/crossclient/crossclient.py
def __init__(
    self,
    username: str,
    password: str,
    base_url: str = DEFAULT_URL,
    verify: bool = True,
) -> None:
    """Initialize the client with authentication.

    Args:
        username (str): The username for authentication.
        password (str): The password for authentication.
        base_url (str): If provided, use this domain instead of the default
            DEFAULT_URL.
            The domain must include the protocol (e.g., http:// or https://).
            Example: "http://example.com".
            Defaults to DEFAULT_URL: "https://backend.sweet-cross.ch".
            Trailing slashes are stripped internally.
        verify (bool): Whether to verify SSL certificates.
            Defaults to True.

    Returns:
        CrossClient: An instance of the authenticated client.
    """
    self._base_url = base_url.rstrip("/")  # Ensure no trailing slash
    self._username = username
    self._password = password
    self._verify = verify
    self._token = None

    # Create the client
    timeout = httpx.Timeout(10.0, connect=30.0, read=60.0, write=None)
    limits = httpx.Limits(max_connections=5, max_keepalive_connections=5)
    self._client = httpx.Client(
        base_url=self._base_url,
        verify=verify,
        timeout=timeout,
        limits=limits,
    )
    self._is_closed = False

    # ---- include services ----
    self.contracts: ContractService = ContractService(client=self)
    self.submissions: SubmissionContractService = SubmissionContractService(
        client=self
    )

    # authenticate upon initialization
    self.authenticate()

    # Register cleanup on interpreter shutdown
    atexit.register(self.close)

authenticate()

Authenticate with the server and retrieve an access token.

Returns:

Name Type Description
str str

The authentication token.

Source code in src/crosscontract/crossclient/crossclient.py
def authenticate(self) -> str:
    """Authenticate with the server and retrieve an access token.

    Returns:
        str: The authentication token.
    """
    response = self._client.post(
        "/user/auth/login",
        data={"username": self._username, "password": self._password},
    )
    response.raise_for_status()  # Raise an error for bad responses
    token = response.json().get("access_token", "")
    self._token = token
    self._client.headers["Authorization"] = f"Bearer {self._token}"
    return token

close()

Close the HTTPX client.

Source code in src/crosscontract/crossclient/crossclient.py
def close(self):
    """Close the HTTPX client."""
    self._client.close()
    self._is_closed = True

delete(endpoint, **kwargs)

Send a DELETE request to the specified endpoint.

Source code in src/crosscontract/crossclient/crossclient.py
def delete(self, endpoint: str, **kwargs) -> httpx.Response:
    """Send a DELETE request to the specified endpoint."""
    return self.request("DELETE", endpoint, **kwargs)  # pragma: no cover

get(endpoint, **kwargs)

Send a GET request to the specified endpoint.

Source code in src/crosscontract/crossclient/crossclient.py
def get(self, endpoint: str, **kwargs) -> httpx.Response:
    """Send a GET request to the specified endpoint."""
    return self.request("GET", endpoint, **kwargs)  # pragma: no cover

patch(endpoint, json=None, **kwargs)

Send a PATCH request to the specified endpoint.

Source code in src/crosscontract/crossclient/crossclient.py
def patch(
    self, endpoint: str, json: dict | None = None, **kwargs
) -> httpx.Response:
    """Send a PATCH request to the specified endpoint."""
    return self.request("PATCH", endpoint, json=json, **kwargs)  # pragma: no cover

post(endpoint, json=None, **kwargs)

Send a POST request to the specified endpoint.

Source code in src/crosscontract/crossclient/crossclient.py
def post(self, endpoint: str, json: dict | None = None, **kwargs) -> httpx.Response:
    """Send a POST request to the specified endpoint."""
    return self.request("POST", endpoint, json=json, **kwargs)  # pragma: no cover

request(method, endpoint, **kwargs)

Send an HTTP request to the specified endpoint.

Parameters:

Name Type Description Default
method str

The HTTP method (e.g., 'GET', 'POST').

required
endpoint str

The API endpoint to send the request to.

required
**kwargs Any

Additional arguments to pass to the request.

{}

Returns:

Type Description
Response

httpx.Response: The response from the server.

Source code in src/crosscontract/crossclient/crossclient.py
def request(self, method: str, endpoint: str, **kwargs: Any) -> httpx.Response:
    """Send an HTTP request to the specified endpoint.

    Args:
        method (str): The HTTP method (e.g., 'GET', 'POST').
        endpoint (str): The API endpoint to send the request to.
        **kwargs: Additional arguments to pass to the request.

    Returns:
        httpx.Response: The response from the server.
    """
    if self._is_closed:
        raise RuntimeError(
            "Attempted to make a request with a closed CrossClient. Ensure you "
            "are performing all operations within the 'with' context block."
        )
    if not self._token:
        self.authenticate()
    response = self._client.request(method, endpoint, **kwargs)

    # try to get a new token if unauthorized
    if response.status_code == 401:
        # Token expired: Refresh and retry
        self.authenticate()

        # Re-issue the request with the new header (handled by self._client update)
        # We must recreate the request to pick up the new headers from the
        # client state
        response = self._client.request(method, endpoint, **kwargs)
    return response

Entry point for operations on the collection of contracts.

Source code in src/crosscontract/crossclient/services/contract/contract_service.py
class ContractService:
    """
    Entry point for operations on the collection of contracts.
    """

    _api_version_prefix = "/api/v1"

    def __init__(self, client: "CrossClient"):
        """Initialize the ContractService. The ContractService is responsible for
        managing contracts on the CROSS platform. It provides methods to create,
        retrieve, list, and delete contracts.

        Args:
            client (CrossClient): The CrossClient instance to use for API calls.
        """
        self._client = client
        self._route = f"{self._client._base_url}{self._api_version_prefix}/contract/"

    def create(
        self, contract: CrossContract, activate: bool = False
    ) -> ContractResource:
        """Create a new contract on the CROSS platform

        Args:
            contract (CrossContract): The contract data to create.
            activate (bool): Whether to activate the contract upon creation.
                Defaults to False.

        Raises:
            httpx.HTTPStatusError: If the request fails.

        Returns:
            ContractResource: The created contract object.
        """
        # 1. Create the contract on the platform
        json_payload = contract.to_server()
        response = self._client.post(self._route, json=json_payload)
        raise_from_response(response)

        # 2. Build the resource from the response payload
        resource = ContractResource.from_response(self, response.json())

        # 3. Activate the contract if requested
        if activate:
            resource.change_status("Active")

        return resource

    def overview(self) -> pd.DataFrame:
        """Get a DataFrame with an overview of all contracts, their status, and
        metadata.

        Returns:
            pd.DataFrame: DataFrame containing contract overviews.
        """
        endpoint = f"{self._route}metadata"
        response = self._client.get(endpoint)
        raise_from_response(response)
        df = pd.DataFrame(response.json())
        return df

    def get_list(
        self, contract_type: list[ContractType] | None = None
    ) -> dict[str, ContractResource]:
        """
        Lists all available contracts as ContractResource objects.

        Args:
            contract_type (list[str] | None): Optional filter restricting the
                result to one or more contract types (e.g.
                `["General", "Dimension"]`). If None, contracts of every type
                are returned.

        Returns:
            dict[str, ContractResource]: Dictionary of contract resources keyed
                by contract name.
        """
        endpoint = self._route
        params: dict[str, Any] = {}
        if contract_type:
            params["contract_type"] = contract_type
        response = self._client.get(endpoint, params=params or None)
        raise_from_response(response)
        json_body = response.json()
        return {
            item["name"]: ContractResource.from_response(self, item)
            for item in json_body
        }

    def get(self, name: str) -> ContractResource:
        """Get contract from the CROSS platform by name.

        Args:
            name (str): The name of the contract.

        Raises:
            httpx.HTTPStatusError: If the request fails.

        Returns:
            ContractResource: The contract resource object.
        """
        endpoint = f"{self._route}{name}"
        response = self._client.get(endpoint)
        raise_from_response(response)
        return ContractResource.from_response(self, response.json())

    def delete(self, name: str, hard: bool = False) -> None:
        """Delete a contract by name if it exists. A contract can only be deleted
        if:
        1. Contract is in "Draft" status
        2. Contract status is "Retired" and the data associated with the contract
            is deleted

        If `hard` is set to True, the contract and all associated data will be deleted.
        Note: This is a dangerous operation and should be used with caution. Usually,
            admin rights are required.

        Args:
            name (str): The name of the contract to delete.
            hard (bool): Whether to perform a hard delete (including data).
                Note: This is a dangerous operation and should be used with caution.
                    it will delete all data associated with the contract. Usually,
                    admin rights are required.
        """
        if hard:
            # if in active or suspended status, change to retired first
            # if in draft status, this will raise an error, which is fine
            try:
                self.change_status(name, "Retired")
            except Exception:
                pass
            # delete all associated data
            try:
                self._drop_data_table(name)
            except Exception:
                pass
        # delete the contract
        try:
            res = self._client.delete(f"{self._route}{name}")
            raise_from_response(res)
        except ResourceNotFoundError:
            # be silent if the contract does not exist
            return

    def change_status(
        self,
        name: str,
        status: ContractStatus,
    ) -> str:
        """Change the status of a contract. Allowable status transitions are enforced
        by the CROSS platform. The allowable statuses are:
            1. Draft
            2. Active
            3. Suspended
            4. Retired
        Allowed transitions:
            - Draft -> Active
            - Active -> Suspended
            - Suspended -> Active
            - Active -> Retired
            - Suspended -> Retired

        Args:
            name (str): The name of the contract to change status.
            status (ContractStatus): The new status for the contract.

        Raises:
            httpx.HTTPStatusError: If the request fails.

        Returns:
            str: The updated status of the contract.
        """
        payload = {"status": status}
        res = self._client.patch(f"{self._route}{name}/state", json=payload)
        raise_from_response(res)
        return res.json()

    def _drop_data_table(self, name: str) -> None:
        """Drop the table storing the data for the given contract.

        This is a decommissioning operation: it discards the data of **every**
        project that submitted under the contract, not only the caller's, and
        requires the contract to be `Retired`. It is restricted to
        administrators and is irreversible. `_delete_data` is the narrower,
        separate operation: it removes only the rows one project owns and
        requires the contract to be `Active`.

        Args:
            name (str): The name of the contract whose data table to drop.

        Raises:
            CrossClientError: If the request fails. Raised via
                `raise_from_response` as a more specific client exception
                such as `ResourceNotFoundError` or `ConflictError`.
        """
        # delete the contract
        res = self._client.delete(f"{self._route}{name}/storage")
        raise_from_response(res)

    def _add_data(
        self, name: str, data: pd.DataFrame, *, project_name: str | None = None
    ) -> None:
        """Add data for the contract on the CROSS platform. Note that this method
        does not perform schema validation. Use ContractResource.add_data() to
        validate data against the contract schema before uploading. I.e., it is
        better to use:
            contract = client.contracts.get(name)
            contract.add_data(data)  # <-- performs validation

        Args:
            name (str): The name of the contract to add data to.
            data (pd.DataFrame): The data to be added.
            project_name (str | None): Optional project name under which the data
                are submitted. If None, the CROSS platform infers the project from the
                caller's memberships, which succeeds only when there is exactly one.

        Raises:
            CrossClientError: If the request fails.
        """
        endpoint = f"{self._route}{name}/data"

        params = {"project_name": project_name} if project_name is not None else None

        # construct the payload as parquet for type safety and efficiency.
        with io.BytesIO() as buffer:
            data.to_parquet(buffer, index=False)
            buffer.seek(0)
            files = {
                "file": (f"{name}.parquet", buffer, "application/vnd.apache.parquet")
            }
            res = self._client.post(endpoint, files=files, params=params)
        raise_from_response(res)
        return

    def _get_data(
        self,
        name: str,
        columns: list[str] | None = None,
        filters: dict[str, str] | None = None,
        unique: bool = False,
    ) -> pd.DataFrame:
        """Get data for the contract from the CROSS platform.

        Args:
            name (str): The name of the contract to get data for.
            columns (list[str] | None): Optional list of columns to retrieve.
                If None, all columns are retrieved.
            filters (dict[str, str] | None): Optional dictionary of filters to apply.
                The keys are column names and the values are the filter values.
                Currently, only equality filters are supported and only one value per
                filter.
            unique (bool): Whether to return only unique rows.

        Returns:
            pd.DataFrame: The data associated with the contract.
        """
        endpoint = f"{self._route}{name}/data"
        params: dict[str, Any] = {}
        if columns:
            params["columns"] = ",".join(columns)
        if filters:
            for key, value in filters.items():
                params[key] = value
        if unique:
            params["unique"] = "true"

        # perform the request using parquet as data format for efficiency
        params["format"] = "parquet"
        response = self._client.get(endpoint, params=params)
        raise_from_response(response)
        df = pd.read_parquet(io.BytesIO(response.content))
        return df

    def _delete_data(
        self,
        name: str,
        filters: dict[str, FilterValue | list[FilterValue]],
        *,
        project_name: str | None = None,
        confirm_delete_all: bool = False,
    ) -> None:
        """Delete rows for the contract matching the given equality filters.

        Filters must be non-empty unless `confirm_delete_all` is set, which
        removes every row the resolved project owns under this contract while
        leaving the table in place. `_drop_data_table` is the wider, separate
        operation: it drops the whole table across every project and requires
        the contract to be `Retired`.

        Values may be str/int/float/bool (or lists thereof for multi-value
        equality) and are stringified before being sent as query parameters.
        List values produce repeated query params (e.g. `?col=a&col=b`),
        which the CROSS platform interprets as an equality match against any
        of the listed values.

        Args:
            name (str): The name of the contract whose rows to delete.
            filters (dict): Mapping of column name to value (or list of values)
                to match. Must be non-empty unless `confirm_delete_all` is
                set.
            project_name (str | None): Optional project name for which project the
                data are deleted. If None, the CROSS platform infers the project
                from the caller's memberships, which succeeds only when there is
                exactly one.
            confirm_delete_all (bool): Confirms that an unfiltered delete is
                intended, removing every row the resolved project owns under
                this contract. Required when `filters` is empty, so that a
                filter mapping which collapsed to empty cannot wipe the
                project's rows by accident. Ignored when `filters` is
                non-empty — the flag is then not sent at all, so a filtered
                delete stays filtered regardless of how the CROSS platform
                orders the two. Defaults to `False`.

        Raises:
            ValueError: If `filters` is empty and `confirm_delete_all` is
                False.
            CrossClientError: If the request fails. Raised via
                `raise_from_response` as a more specific client exception
                such as `ResourceNotFoundError`, `ConflictError`, or
                `ServerError`.
        """
        if not filters and not confirm_delete_all:
            raise ValueError(
                "Filters must be non-empty unless confirm_delete_all=True is specified"
            )
        params: dict[str, str | list[str]] = {
            k: [str(x) for x in v] if isinstance(v, list) else str(v)
            for k, v in filters.items()
        }
        if project_name is not None:
            params["project_name"] = project_name
        # Only an unfiltered delete carries the flag: sending it alongside
        # filters would make the scope of the deletion depend on how the
        # platform prioritises the two, and the fallout of guessing wrong is
        # every row the project owns.
        if confirm_delete_all and not filters:
            params["delete_all"] = "True"
        endpoint = f"{self._route}{name}/data"
        response = self._client.delete(endpoint, params=params)
        raise_from_response(response)

__init__(client)

Initialize the ContractService. The ContractService is responsible for managing contracts on the CROSS platform. It provides methods to create, retrieve, list, and delete contracts.

Parameters:

Name Type Description Default
client CrossClient

The CrossClient instance to use for API calls.

required
Source code in src/crosscontract/crossclient/services/contract/contract_service.py
def __init__(self, client: "CrossClient"):
    """Initialize the ContractService. The ContractService is responsible for
    managing contracts on the CROSS platform. It provides methods to create,
    retrieve, list, and delete contracts.

    Args:
        client (CrossClient): The CrossClient instance to use for API calls.
    """
    self._client = client
    self._route = f"{self._client._base_url}{self._api_version_prefix}/contract/"

change_status(name, status)

Change the status of a contract. Allowable status transitions are enforced by the CROSS platform. The allowable statuses are: 1. Draft 2. Active 3. Suspended 4. Retired Allowed transitions: - Draft -> Active - Active -> Suspended - Suspended -> Active - Active -> Retired - Suspended -> Retired

Parameters:

Name Type Description Default
name str

The name of the contract to change status.

required
status ContractStatus

The new status for the contract.

required

Raises:

Type Description
HTTPStatusError

If the request fails.

Returns:

Name Type Description
str str

The updated status of the contract.

Source code in src/crosscontract/crossclient/services/contract/contract_service.py
def change_status(
    self,
    name: str,
    status: ContractStatus,
) -> str:
    """Change the status of a contract. Allowable status transitions are enforced
    by the CROSS platform. The allowable statuses are:
        1. Draft
        2. Active
        3. Suspended
        4. Retired
    Allowed transitions:
        - Draft -> Active
        - Active -> Suspended
        - Suspended -> Active
        - Active -> Retired
        - Suspended -> Retired

    Args:
        name (str): The name of the contract to change status.
        status (ContractStatus): The new status for the contract.

    Raises:
        httpx.HTTPStatusError: If the request fails.

    Returns:
        str: The updated status of the contract.
    """
    payload = {"status": status}
    res = self._client.patch(f"{self._route}{name}/state", json=payload)
    raise_from_response(res)
    return res.json()

create(contract, activate=False)

Create a new contract on the CROSS platform

Parameters:

Name Type Description Default
contract CrossContract

The contract data to create.

required
activate bool

Whether to activate the contract upon creation. Defaults to False.

False

Raises:

Type Description
HTTPStatusError

If the request fails.

Returns:

Name Type Description
ContractResource ContractResource

The created contract object.

Source code in src/crosscontract/crossclient/services/contract/contract_service.py
def create(
    self, contract: CrossContract, activate: bool = False
) -> ContractResource:
    """Create a new contract on the CROSS platform

    Args:
        contract (CrossContract): The contract data to create.
        activate (bool): Whether to activate the contract upon creation.
            Defaults to False.

    Raises:
        httpx.HTTPStatusError: If the request fails.

    Returns:
        ContractResource: The created contract object.
    """
    # 1. Create the contract on the platform
    json_payload = contract.to_server()
    response = self._client.post(self._route, json=json_payload)
    raise_from_response(response)

    # 2. Build the resource from the response payload
    resource = ContractResource.from_response(self, response.json())

    # 3. Activate the contract if requested
    if activate:
        resource.change_status("Active")

    return resource

delete(name, hard=False)

Delete a contract by name if it exists. A contract can only be deleted if: 1. Contract is in "Draft" status 2. Contract status is "Retired" and the data associated with the contract is deleted

If hard is set to True, the contract and all associated data will be deleted. Note: This is a dangerous operation and should be used with caution. Usually, admin rights are required.

Parameters:

Name Type Description Default
name str

The name of the contract to delete.

required
hard bool

Whether to perform a hard delete (including data). Note: This is a dangerous operation and should be used with caution. it will delete all data associated with the contract. Usually, admin rights are required.

False
Source code in src/crosscontract/crossclient/services/contract/contract_service.py
def delete(self, name: str, hard: bool = False) -> None:
    """Delete a contract by name if it exists. A contract can only be deleted
    if:
    1. Contract is in "Draft" status
    2. Contract status is "Retired" and the data associated with the contract
        is deleted

    If `hard` is set to True, the contract and all associated data will be deleted.
    Note: This is a dangerous operation and should be used with caution. Usually,
        admin rights are required.

    Args:
        name (str): The name of the contract to delete.
        hard (bool): Whether to perform a hard delete (including data).
            Note: This is a dangerous operation and should be used with caution.
                it will delete all data associated with the contract. Usually,
                admin rights are required.
    """
    if hard:
        # if in active or suspended status, change to retired first
        # if in draft status, this will raise an error, which is fine
        try:
            self.change_status(name, "Retired")
        except Exception:
            pass
        # delete all associated data
        try:
            self._drop_data_table(name)
        except Exception:
            pass
    # delete the contract
    try:
        res = self._client.delete(f"{self._route}{name}")
        raise_from_response(res)
    except ResourceNotFoundError:
        # be silent if the contract does not exist
        return

get(name)

Get contract from the CROSS platform by name.

Parameters:

Name Type Description Default
name str

The name of the contract.

required

Raises:

Type Description
HTTPStatusError

If the request fails.

Returns:

Name Type Description
ContractResource ContractResource

The contract resource object.

Source code in src/crosscontract/crossclient/services/contract/contract_service.py
def get(self, name: str) -> ContractResource:
    """Get contract from the CROSS platform by name.

    Args:
        name (str): The name of the contract.

    Raises:
        httpx.HTTPStatusError: If the request fails.

    Returns:
        ContractResource: The contract resource object.
    """
    endpoint = f"{self._route}{name}"
    response = self._client.get(endpoint)
    raise_from_response(response)
    return ContractResource.from_response(self, response.json())

get_list(contract_type=None)

Lists all available contracts as ContractResource objects.

Parameters:

Name Type Description Default
contract_type list[str] | None

Optional filter restricting the result to one or more contract types (e.g. ["General", "Dimension"]). If None, contracts of every type are returned.

None

Returns:

Type Description
dict[str, ContractResource]

dict[str, ContractResource]: Dictionary of contract resources keyed by contract name.

Source code in src/crosscontract/crossclient/services/contract/contract_service.py
def get_list(
    self, contract_type: list[ContractType] | None = None
) -> dict[str, ContractResource]:
    """
    Lists all available contracts as ContractResource objects.

    Args:
        contract_type (list[str] | None): Optional filter restricting the
            result to one or more contract types (e.g.
            `["General", "Dimension"]`). If None, contracts of every type
            are returned.

    Returns:
        dict[str, ContractResource]: Dictionary of contract resources keyed
            by contract name.
    """
    endpoint = self._route
    params: dict[str, Any] = {}
    if contract_type:
        params["contract_type"] = contract_type
    response = self._client.get(endpoint, params=params or None)
    raise_from_response(response)
    json_body = response.json()
    return {
        item["name"]: ContractResource.from_response(self, item)
        for item in json_body
    }

overview()

Get a DataFrame with an overview of all contracts, their status, and metadata.

Returns:

Type Description
DataFrame

pd.DataFrame: DataFrame containing contract overviews.

Source code in src/crosscontract/crossclient/services/contract/contract_service.py
def overview(self) -> pd.DataFrame:
    """Get a DataFrame with an overview of all contracts, their status, and
    metadata.

    Returns:
        pd.DataFrame: DataFrame containing contract overviews.
    """
    endpoint = f"{self._route}metadata"
    response = self._client.get(endpoint)
    raise_from_response(response)
    df = pd.DataFrame(response.json())
    return df

A handle to a contract that exists on the CROSS platform.

ContractResources are read-only wrappers around contract data fetched from the CROSS platform. They are produced exclusively by ContractService methods (create, get, get_list); end users do not construct them directly.

Attributes:

Name Type Description
name str

The name of the contract.

status str

The status of the contract.

contract CrossContract

The full contract details.

contract_type str

The type of the contract, e.g. "General".

service ContractService

The owning ContractService.

Source code in src/crosscontract/crossclient/services/contract/contract_resource.py
class ContractResource:
    """A handle to a contract that exists on the CROSS platform.

    ContractResources are read-only wrappers around contract data fetched from
    the CROSS platform. They are produced exclusively by `ContractService`
    methods (`create`, `get`, `get_list`); end users do not construct them
    directly.

    Attributes:
        name (str): The name of the contract.
        status (str): The status of the contract.
        contract (CrossContract): The full contract details.
        contract_type (str): The type of the contract, e.g. `"General"`.
        service (ContractService): The owning `ContractService`.
    """

    def __init__(
        self,
        service: "ContractService",
        payload: _ContractEntryPayload,
    ):
        """Initialise from a parsed server payload.

        Most callers should use :meth:`from_response` to parse a raw JSON dict;
        this constructor takes a pre-validated payload to keep tests direct.
        """
        self._service = service
        self._name = payload.name
        self._status = payload.status
        self._contract_type = payload.contract_type
        self._contract = CrossContract.from_server(payload.contract)

    @classmethod
    def from_response(
        cls,
        service: "ContractService",
        response_json: dict[str, Any],
    ) -> "ContractResource":
        """Build a ContractResource from a raw server response dict."""
        payload = _ContractEntryPayload.model_validate(response_json)
        return cls(service, payload)

    @property
    def name(self) -> str:
        return self._name

    @property
    def status(self) -> ContractStatus:
        return self._status

    @property
    def contract_type(self) -> str:
        return self._contract_type

    @property
    def contract(self) -> CrossContract:
        return self._contract

    @property
    def is_dimension(self) -> bool:
        """True if the contract's tableschema is a dimension schema."""
        return isinstance(self._contract.tableschema, BaseDimensionSchema)

    def __setattr__(self, name, value):
        # 1. Access the class to find the attribute definition
        # We use type(self) to avoid triggering infinite recursion or property getters
        attr = getattr(type(self), name, None)

        # 2. Check if the attribute is a property and if it has no setter
        if isinstance(attr, property) and attr.fset is None:
            raise AttributeError(
                "ContractResource is read-only. Use the methods to update properties."
            )

        # 3. If it's not a read-only property, allow the default behavior
        # This allows setting private variables like self._x = 10
        super().__setattr__(name, value)

    def __repr__(self):
        return f"ContractResource(name={self.name}, status={self.status})"

    def change_status(self, status: ContractStatus) -> None:
        """Change the status of the contract.

        Args:
            status (ContractStatus): The new status for the contract.
        """
        self._service.change_status(self.name, status)
        self._status = status

    def refresh(self) -> None:
        """Re-fetch the contract details from the CROSS platform."""
        remote = self._service.get(self.name)
        if remote.name != self.name:
            raise ValueError(
                f"Fetched contract name '{remote.name}' does not match "
                f"resource name '{self.name}'."
            )
        self._contract = remote.contract
        self._status = remote.status
        self._contract_type = remote.contract_type

    def _prepare_dataframe_csv_upload(self, df: pd.DataFrame) -> pd.DataFrame:
        """Prepare a DataFrame for CSV upload by formatting datetime columns.

        This method converts datetime-typed fields defined in the contract's
        table schema from pandas datetime dtypes to string values using the
        field's configured format. Columns of other data types are left
        unchanged.

        Args:
            df (pd.DataFrame): The input DataFrame to be prepared for CSV upload.

        Returns:
            pd.DataFrame: The prepared DataFrame ready for CSV upload.
        """
        # convert datetime fields to string with correct format
        dt_fields = [
            f
            for f in self.contract.tableschema.field_iterator()
            if f.type == "datetime" and f.name in df.columns
        ]
        if len(dt_fields) == 0:
            return df
        df_out = df.copy(deep=False)
        for field in dt_fields:
            if pd.api.types.is_datetime64_any_dtype(df[field.name]):
                df_out[field.name] = df_out[field.name].dt.strftime(field.format)
        return df_out

    def add_data(
        self,
        data: pd.DataFrame,
        validate: bool = True,
        *,
        project_name: str | None = None,
    ) -> None:
        """Add data for the contract on the CROSS platform.

        The rows are stored as owned by the resolved project.

        Args:
            data (pd.DataFrame): The data to be added.
            validate (bool): Whether to validate the data against the contract
                schema before uploading. Defaults to True.
            project_name (str | None): Optional project name under which the data
                are submitted. If None, the CROSS platform infers the project from the
                caller's memberships, which succeeds only when there is exactly one.

        Raises:
            ValidationError: If the data does not conform to the contract schema.
            CrossClientError: If the upload fails. Raised via
                `raise_from_response` as a more specific client exception such
                as `ResourceNotFoundError` or `ConflictError`.
        """
        if validate:
            # validate data against contract schema at the client side
            self.validate_dataframe(data)
        data_out = self._prepare_dataframe_csv_upload(data)
        self._service._add_data(self.name, data_out, project_name=project_name)

    def get_data(
        self,
        columns: list[str] | None = None,
        filters: dict[str, str] | None = None,
        unique: bool = False,
    ) -> pd.DataFrame:
        """Get data for the contract from the CROSS platform.

        Args:
            columns (list[str] | None): Optional list of columns to retrieve.
                If None, all columns are retrieved.
            filters (dict[str, str] | None): Optional dictionary of filters to apply.
                The keys are column names and the values are the filter values.
                Currently, only equality filters are supported and only one value per
                filter.
            unique (bool): Whether to return only unique rows.

        Returns:
            pd.DataFrame: The data associated with the contract.
        """
        return self._service._get_data(
            name=self.name, columns=columns, filters=filters, unique=unique
        )

    def validate_dataframe(
        self,
        df: pd.DataFrame,
        check_existing_primary_key: bool = False,
        check_existing_foreign_key: bool = False,
        check_dimension_granularity: bool = False,
        lazy: bool = True,
    ) -> None:
        """Validate a DataFrame against the schema of the contract.

        By default nothing is read from the CROSS platform and the data is
        validated on its own. Setting a check flag reads from it: the primary key
        is then checked against the union of the stored keys and the DataFrame's
        own, the foreign keys against the stored values of the contracts they
        reference — plus the DataFrame's own rows in the case of a
        self-referencing foreign key — and the granularity check reads the
        hierarchy of each referenced dimension.

        Args:
            df (pd.DataFrame): The DataFrame to validate.
            check_existing_primary_key (bool): If True, also check the primary
                key against the values already stored for this contract.
                Default is False.
            check_existing_foreign_key (bool): If True, also check the foreign
                keys against the values already stored for the contracts they
                reference. Default is False.
            check_dimension_granularity (bool): If True, also check that no group
                of otherwise-identical rows reports a member of a hierarchical
                dimension alongside one of its descendants, which would count
                that member twice when the data is summed. Only a ValueVariable
                is checked, and only its references to a `Dimension`; a
                `FlexibleDimension` is flat and has nothing to check.
                Default is False.
            lazy (bool): If True, collect all validation errors and raise them together.
                If False, raise the first validation error encountered.
                Default is True.

        Raises:
            ValidationError: If the DataFrame does not conform to the schema.
            CrossClientError: If fetching the stored values fails. Raised via
                `raise_from_response` as a more specific client exception such
                as `ResourceNotFoundError` when the contract has no stored data
                yet.
        """
        resolver = CrossContractResolver(self._service)
        try:
            self.contract.validate_data(
                df,
                resolver=resolver,
                check_existing_primary_key=check_existing_primary_key,
                check_existing_foreign_key=check_existing_foreign_key,
                check_dimension_granularity=check_dimension_granularity,
                lazy=lazy,
            )
        except SchemaValidationError as e:
            # convert to CrossClient ValidationError
            raise ValidationError(
                message=f"DataFrame validation against contract '{self.name}' "
                "schema failed.",
                validation_errors=e.to_list(),
            ) from e

    def drop_data(self) -> None:
        """Drop the storage table backing the contract on the CROSS platform.

        This is a decommissioning operation: it discards the data of **every**
        project that submitted under this contract, not only the caller's, and
        requires the contract to be `Retired`. It is restricted to
        administrators. To remove only the rows owned by one project, use
        `delete_data()` — with `confirm_delete_all=True` to clear that
        project's rows entirely.

        Raises:
            CrossClientError: If the request fails. Raised via
                `raise_from_response` as a more specific client exception such
                as `ResourceNotFoundError` or `ConflictError`.
        """
        self._service._drop_data_table(self.name)

    def delete_data(
        self,
        filters: "dict[str, FilterValue | list[FilterValue]]",
        *,
        project_name: str | None = None,
        confirm_delete_all: bool = False,
    ) -> None:
        """Delete rows from the contract's data matching the given equality filters.

        Only rows owned by the resolved project are removed, and the contract
        must be `Active`. `drop_data()` is the wider, separate operation: it
        drops the whole storage table across every project and requires the
        contract to be `Retired`.

        Args:
            filters (dict): Mapping of column name to value (or list of values)
                to match. Values may be str/int/float/bool. Must be non-empty
                unless `confirm_delete_all` is set.
            project_name (str | None): Optional project name for which project the
                data are deleted. If None, the CROSS platform infers the project
                from the caller's memberships, which succeeds only when there is
                exactly one.
            confirm_delete_all (bool): Confirms that an unfiltered delete is
                intended, removing every row the resolved project owns under
                this contract. Required when `filters` is empty, so that a
                filter mapping which collapsed to empty cannot wipe the
                project's rows by accident. Ignored when `filters` is
                non-empty — a filtered delete stays filtered, and the
                confirmation never reaches the CROSS platform. Defaults to
                `False`.

        Raises:
            ValueError: If the contract's cached status is not `"Active"`, or
                if `filters` is empty and `confirm_delete_all` is False. The
                status check is local and uses the cached status; call
                `refresh()` first if the status may have changed on the CROSS
                platform.
            CrossClientError: Propagated from the underlying service/HTTP
                request if the deletion request fails due to client, server,
                or network-related errors.
        """
        if self._status != "Active":
            raise ValueError(
                f"Cannot delete data from contract '{self.name}': status is "
                f"'{self._status}', must be 'Active'. Call refresh() if the "
                "status may have changed on the server."
            )
        self._service._delete_data(
            self.name,
            filters,
            project_name=project_name,
            confirm_delete_all=confirm_delete_all,
        )

is_dimension property

True if the contract's tableschema is a dimension schema.

__init__(service, payload)

Initialise from a parsed server payload.

Most callers should use :meth:from_response to parse a raw JSON dict; this constructor takes a pre-validated payload to keep tests direct.

Source code in src/crosscontract/crossclient/services/contract/contract_resource.py
def __init__(
    self,
    service: "ContractService",
    payload: _ContractEntryPayload,
):
    """Initialise from a parsed server payload.

    Most callers should use :meth:`from_response` to parse a raw JSON dict;
    this constructor takes a pre-validated payload to keep tests direct.
    """
    self._service = service
    self._name = payload.name
    self._status = payload.status
    self._contract_type = payload.contract_type
    self._contract = CrossContract.from_server(payload.contract)

add_data(data, validate=True, *, project_name=None)

Add data for the contract on the CROSS platform.

The rows are stored as owned by the resolved project.

Parameters:

Name Type Description Default
data DataFrame

The data to be added.

required
validate bool

Whether to validate the data against the contract schema before uploading. Defaults to True.

True
project_name str | None

Optional project name under which the data are submitted. If None, the CROSS platform infers the project from the caller's memberships, which succeeds only when there is exactly one.

None

Raises:

Type Description
ValidationError

If the data does not conform to the contract schema.

CrossClientError

If the upload fails. Raised via raise_from_response as a more specific client exception such as ResourceNotFoundError or ConflictError.

Source code in src/crosscontract/crossclient/services/contract/contract_resource.py
def add_data(
    self,
    data: pd.DataFrame,
    validate: bool = True,
    *,
    project_name: str | None = None,
) -> None:
    """Add data for the contract on the CROSS platform.

    The rows are stored as owned by the resolved project.

    Args:
        data (pd.DataFrame): The data to be added.
        validate (bool): Whether to validate the data against the contract
            schema before uploading. Defaults to True.
        project_name (str | None): Optional project name under which the data
            are submitted. If None, the CROSS platform infers the project from the
            caller's memberships, which succeeds only when there is exactly one.

    Raises:
        ValidationError: If the data does not conform to the contract schema.
        CrossClientError: If the upload fails. Raised via
            `raise_from_response` as a more specific client exception such
            as `ResourceNotFoundError` or `ConflictError`.
    """
    if validate:
        # validate data against contract schema at the client side
        self.validate_dataframe(data)
    data_out = self._prepare_dataframe_csv_upload(data)
    self._service._add_data(self.name, data_out, project_name=project_name)

change_status(status)

Change the status of the contract.

Parameters:

Name Type Description Default
status ContractStatus

The new status for the contract.

required
Source code in src/crosscontract/crossclient/services/contract/contract_resource.py
def change_status(self, status: ContractStatus) -> None:
    """Change the status of the contract.

    Args:
        status (ContractStatus): The new status for the contract.
    """
    self._service.change_status(self.name, status)
    self._status = status

delete_data(filters, *, project_name=None, confirm_delete_all=False)

Delete rows from the contract's data matching the given equality filters.

Only rows owned by the resolved project are removed, and the contract must be Active. drop_data() is the wider, separate operation: it drops the whole storage table across every project and requires the contract to be Retired.

Parameters:

Name Type Description Default
filters dict

Mapping of column name to value (or list of values) to match. Values may be str/int/float/bool. Must be non-empty unless confirm_delete_all is set.

required
project_name str | None

Optional project name for which project the data are deleted. If None, the CROSS platform infers the project from the caller's memberships, which succeeds only when there is exactly one.

None
confirm_delete_all bool

Confirms that an unfiltered delete is intended, removing every row the resolved project owns under this contract. Required when filters is empty, so that a filter mapping which collapsed to empty cannot wipe the project's rows by accident. Ignored when filters is non-empty — a filtered delete stays filtered, and the confirmation never reaches the CROSS platform. Defaults to False.

False

Raises:

Type Description
ValueError

If the contract's cached status is not "Active", or if filters is empty and confirm_delete_all is False. The status check is local and uses the cached status; call refresh() first if the status may have changed on the CROSS platform.

CrossClientError

Propagated from the underlying service/HTTP request if the deletion request fails due to client, server, or network-related errors.

Source code in src/crosscontract/crossclient/services/contract/contract_resource.py
def delete_data(
    self,
    filters: "dict[str, FilterValue | list[FilterValue]]",
    *,
    project_name: str | None = None,
    confirm_delete_all: bool = False,
) -> None:
    """Delete rows from the contract's data matching the given equality filters.

    Only rows owned by the resolved project are removed, and the contract
    must be `Active`. `drop_data()` is the wider, separate operation: it
    drops the whole storage table across every project and requires the
    contract to be `Retired`.

    Args:
        filters (dict): Mapping of column name to value (or list of values)
            to match. Values may be str/int/float/bool. Must be non-empty
            unless `confirm_delete_all` is set.
        project_name (str | None): Optional project name for which project the
            data are deleted. If None, the CROSS platform infers the project
            from the caller's memberships, which succeeds only when there is
            exactly one.
        confirm_delete_all (bool): Confirms that an unfiltered delete is
            intended, removing every row the resolved project owns under
            this contract. Required when `filters` is empty, so that a
            filter mapping which collapsed to empty cannot wipe the
            project's rows by accident. Ignored when `filters` is
            non-empty — a filtered delete stays filtered, and the
            confirmation never reaches the CROSS platform. Defaults to
            `False`.

    Raises:
        ValueError: If the contract's cached status is not `"Active"`, or
            if `filters` is empty and `confirm_delete_all` is False. The
            status check is local and uses the cached status; call
            `refresh()` first if the status may have changed on the CROSS
            platform.
        CrossClientError: Propagated from the underlying service/HTTP
            request if the deletion request fails due to client, server,
            or network-related errors.
    """
    if self._status != "Active":
        raise ValueError(
            f"Cannot delete data from contract '{self.name}': status is "
            f"'{self._status}', must be 'Active'. Call refresh() if the "
            "status may have changed on the server."
        )
    self._service._delete_data(
        self.name,
        filters,
        project_name=project_name,
        confirm_delete_all=confirm_delete_all,
    )

drop_data()

Drop the storage table backing the contract on the CROSS platform.

This is a decommissioning operation: it discards the data of every project that submitted under this contract, not only the caller's, and requires the contract to be Retired. It is restricted to administrators. To remove only the rows owned by one project, use delete_data() — with confirm_delete_all=True to clear that project's rows entirely.

Raises:

Type Description
CrossClientError

If the request fails. Raised via raise_from_response as a more specific client exception such as ResourceNotFoundError or ConflictError.

Source code in src/crosscontract/crossclient/services/contract/contract_resource.py
def drop_data(self) -> None:
    """Drop the storage table backing the contract on the CROSS platform.

    This is a decommissioning operation: it discards the data of **every**
    project that submitted under this contract, not only the caller's, and
    requires the contract to be `Retired`. It is restricted to
    administrators. To remove only the rows owned by one project, use
    `delete_data()` — with `confirm_delete_all=True` to clear that
    project's rows entirely.

    Raises:
        CrossClientError: If the request fails. Raised via
            `raise_from_response` as a more specific client exception such
            as `ResourceNotFoundError` or `ConflictError`.
    """
    self._service._drop_data_table(self.name)

from_response(service, response_json) classmethod

Build a ContractResource from a raw server response dict.

Source code in src/crosscontract/crossclient/services/contract/contract_resource.py
@classmethod
def from_response(
    cls,
    service: "ContractService",
    response_json: dict[str, Any],
) -> "ContractResource":
    """Build a ContractResource from a raw server response dict."""
    payload = _ContractEntryPayload.model_validate(response_json)
    return cls(service, payload)

get_data(columns=None, filters=None, unique=False)

Get data for the contract from the CROSS platform.

Parameters:

Name Type Description Default
columns list[str] | None

Optional list of columns to retrieve. If None, all columns are retrieved.

None
filters dict[str, str] | None

Optional dictionary of filters to apply. The keys are column names and the values are the filter values. Currently, only equality filters are supported and only one value per filter.

None
unique bool

Whether to return only unique rows.

False

Returns:

Type Description
DataFrame

pd.DataFrame: The data associated with the contract.

Source code in src/crosscontract/crossclient/services/contract/contract_resource.py
def get_data(
    self,
    columns: list[str] | None = None,
    filters: dict[str, str] | None = None,
    unique: bool = False,
) -> pd.DataFrame:
    """Get data for the contract from the CROSS platform.

    Args:
        columns (list[str] | None): Optional list of columns to retrieve.
            If None, all columns are retrieved.
        filters (dict[str, str] | None): Optional dictionary of filters to apply.
            The keys are column names and the values are the filter values.
            Currently, only equality filters are supported and only one value per
            filter.
        unique (bool): Whether to return only unique rows.

    Returns:
        pd.DataFrame: The data associated with the contract.
    """
    return self._service._get_data(
        name=self.name, columns=columns, filters=filters, unique=unique
    )

refresh()

Re-fetch the contract details from the CROSS platform.

Source code in src/crosscontract/crossclient/services/contract/contract_resource.py
def refresh(self) -> None:
    """Re-fetch the contract details from the CROSS platform."""
    remote = self._service.get(self.name)
    if remote.name != self.name:
        raise ValueError(
            f"Fetched contract name '{remote.name}' does not match "
            f"resource name '{self.name}'."
        )
    self._contract = remote.contract
    self._status = remote.status
    self._contract_type = remote.contract_type

validate_dataframe(df, check_existing_primary_key=False, check_existing_foreign_key=False, check_dimension_granularity=False, lazy=True)

Validate a DataFrame against the schema of the contract.

By default nothing is read from the CROSS platform and the data is validated on its own. Setting a check flag reads from it: the primary key is then checked against the union of the stored keys and the DataFrame's own, the foreign keys against the stored values of the contracts they reference — plus the DataFrame's own rows in the case of a self-referencing foreign key — and the granularity check reads the hierarchy of each referenced dimension.

Parameters:

Name Type Description Default
df DataFrame

The DataFrame to validate.

required
check_existing_primary_key bool

If True, also check the primary key against the values already stored for this contract. Default is False.

False
check_existing_foreign_key bool

If True, also check the foreign keys against the values already stored for the contracts they reference. Default is False.

False
check_dimension_granularity bool

If True, also check that no group of otherwise-identical rows reports a member of a hierarchical dimension alongside one of its descendants, which would count that member twice when the data is summed. Only a ValueVariable is checked, and only its references to a Dimension; a FlexibleDimension is flat and has nothing to check. Default is False.

False
lazy bool

If True, collect all validation errors and raise them together. If False, raise the first validation error encountered. Default is True.

True

Raises:

Type Description
ValidationError

If the DataFrame does not conform to the schema.

CrossClientError

If fetching the stored values fails. Raised via raise_from_response as a more specific client exception such as ResourceNotFoundError when the contract has no stored data yet.

Source code in src/crosscontract/crossclient/services/contract/contract_resource.py
def validate_dataframe(
    self,
    df: pd.DataFrame,
    check_existing_primary_key: bool = False,
    check_existing_foreign_key: bool = False,
    check_dimension_granularity: bool = False,
    lazy: bool = True,
) -> None:
    """Validate a DataFrame against the schema of the contract.

    By default nothing is read from the CROSS platform and the data is
    validated on its own. Setting a check flag reads from it: the primary key
    is then checked against the union of the stored keys and the DataFrame's
    own, the foreign keys against the stored values of the contracts they
    reference — plus the DataFrame's own rows in the case of a
    self-referencing foreign key — and the granularity check reads the
    hierarchy of each referenced dimension.

    Args:
        df (pd.DataFrame): The DataFrame to validate.
        check_existing_primary_key (bool): If True, also check the primary
            key against the values already stored for this contract.
            Default is False.
        check_existing_foreign_key (bool): If True, also check the foreign
            keys against the values already stored for the contracts they
            reference. Default is False.
        check_dimension_granularity (bool): If True, also check that no group
            of otherwise-identical rows reports a member of a hierarchical
            dimension alongside one of its descendants, which would count
            that member twice when the data is summed. Only a ValueVariable
            is checked, and only its references to a `Dimension`; a
            `FlexibleDimension` is flat and has nothing to check.
            Default is False.
        lazy (bool): If True, collect all validation errors and raise them together.
            If False, raise the first validation error encountered.
            Default is True.

    Raises:
        ValidationError: If the DataFrame does not conform to the schema.
        CrossClientError: If fetching the stored values fails. Raised via
            `raise_from_response` as a more specific client exception such
            as `ResourceNotFoundError` when the contract has no stored data
            yet.
    """
    resolver = CrossContractResolver(self._service)
    try:
        self.contract.validate_data(
            df,
            resolver=resolver,
            check_existing_primary_key=check_existing_primary_key,
            check_existing_foreign_key=check_existing_foreign_key,
            check_dimension_granularity=check_dimension_granularity,
            lazy=lazy,
        )
    except SchemaValidationError as e:
        # convert to CrossClient ValidationError
        raise ValidationError(
            message=f"DataFrame validation against contract '{self.name}' "
            "schema failed.",
            validation_errors=e.to_list(),
        ) from e

Bases: ContractResolver

Reads contracts and their data from the CROSS platform.

Answers the two questions a contract cannot answer on its own: what another contract looks like, and which values are already stored under it. Used when validating data against a contract that references other contracts.

Reaches the platform over HTTP through a ContractService, so it sees whatever the authenticated caller is allowed to read.

Source code in src/crosscontract/crossclient/services/contract/resolver.py
class CrossContractResolver(ContractResolver):
    """Reads contracts and their data from the CROSS platform.

    Answers the two questions a contract cannot answer on its own: what another
    contract looks like, and which values are already stored under it. Used when
    validating data against a contract that references other contracts.

    Reaches the platform over HTTP through a `ContractService`, so it sees
    whatever the authenticated caller is allowed to read.
    """

    def __init__(self, service: ContractService):
        """Initialise the resolver with the service it reads through.

        Args:
            service (ContractService): The service used to reach the platform.
        """
        self._service = service

    def resolve(self, name: str) -> CrossContract | None:
        """Get a contract by name.

        Args:
            name (str): The name of the contract.

        Returns:
            CrossContract | None: The contract, or `None` if the platform has no
            contract with that name.

        Raises:
            CrossClientError: If the read fails for any reason other than the
                contract being absent. Only a missing contract becomes `None`; a
                permission error or a server failure propagates as the more
                specific client exception.
        """
        try:
            return self._service.get(name).contract
        except ResourceNotFoundError:
            return None

    def get_data(
        self, name: str, columns: list[str], *, unique: bool = True
    ) -> pd.DataFrame:
        """Get the stored values of the given columns for a contract.

        Args:
            name (str): The name of the contract to read from.
            columns (list[str]): The columns to retrieve.
            unique (bool): Whether to return only unique rows. Defaults to
                `True`, which keeps the response small; the result is the same
                either way.

        Returns:
            pd.DataFrame: The requested columns of the named contract.

        Raises:
            CrossClientError: If the read fails. Raised via
                `raise_from_response` as a more specific client exception such
                as `ResourceNotFoundError`.
        """
        return self._service._get_data(name, columns=columns, unique=unique)

__init__(service)

Initialise the resolver with the service it reads through.

Parameters:

Name Type Description Default
service ContractService

The service used to reach the platform.

required
Source code in src/crosscontract/crossclient/services/contract/resolver.py
def __init__(self, service: ContractService):
    """Initialise the resolver with the service it reads through.

    Args:
        service (ContractService): The service used to reach the platform.
    """
    self._service = service

get_data(name, columns, *, unique=True)

Get the stored values of the given columns for a contract.

Parameters:

Name Type Description Default
name str

The name of the contract to read from.

required
columns list[str]

The columns to retrieve.

required
unique bool

Whether to return only unique rows. Defaults to True, which keeps the response small; the result is the same either way.

True

Returns:

Type Description
DataFrame

pd.DataFrame: The requested columns of the named contract.

Raises:

Type Description
CrossClientError

If the read fails. Raised via raise_from_response as a more specific client exception such as ResourceNotFoundError.

Source code in src/crosscontract/crossclient/services/contract/resolver.py
def get_data(
    self, name: str, columns: list[str], *, unique: bool = True
) -> pd.DataFrame:
    """Get the stored values of the given columns for a contract.

    Args:
        name (str): The name of the contract to read from.
        columns (list[str]): The columns to retrieve.
        unique (bool): Whether to return only unique rows. Defaults to
            `True`, which keeps the response small; the result is the same
            either way.

    Returns:
        pd.DataFrame: The requested columns of the named contract.

    Raises:
        CrossClientError: If the read fails. Raised via
            `raise_from_response` as a more specific client exception such
            as `ResourceNotFoundError`.
    """
    return self._service._get_data(name, columns=columns, unique=unique)

resolve(name)

Get a contract by name.

Parameters:

Name Type Description Default
name str

The name of the contract.

required

Returns:

Type Description
CrossContract | None

CrossContract | None: The contract, or None if the platform has no

CrossContract | None

contract with that name.

Raises:

Type Description
CrossClientError

If the read fails for any reason other than the contract being absent. Only a missing contract becomes None; a permission error or a server failure propagates as the more specific client exception.

Source code in src/crosscontract/crossclient/services/contract/resolver.py
def resolve(self, name: str) -> CrossContract | None:
    """Get a contract by name.

    Args:
        name (str): The name of the contract.

    Returns:
        CrossContract | None: The contract, or `None` if the platform has no
        contract with that name.

    Raises:
        CrossClientError: If the read fails for any reason other than the
            contract being absent. Only a missing contract becomes `None`; a
            permission error or a server failure propagates as the more
            specific client exception.
    """
    try:
        return self._service.get(name).contract
    except ResourceNotFoundError:
        return None

Entry point for operations on submission contracts and their submissions.

Source code in src/crosscontract/crossclient/services/submission/submission_service.py
class SubmissionContractService:
    """Entry point for operations on submission contracts and their submissions."""

    _api_version_prefix = "/api/v1"

    def __init__(self, client: "CrossClient"):
        """Initialize the service.

        Args:
            client (CrossClient): The CrossClient instance to use for API calls.
        """
        self._client = client
        self._route = f"{self._client._base_url}{self._api_version_prefix}/submission"

    def create(self, contract: SubmissionContract) -> SubmissionContractResource:
        """Create a submission contract on the CROSS platform.

        Every target of the contract must name an existing, active data contract.
        If reading it back after the creation fails, the submission contract
        already exists; fetch it with `get` instead of calling `create` again.

        Args:
            contract (SubmissionContract): The submission contract to create.

        Returns:
            SubmissionContractResource: The created submission contract.

        Raises:
            CrossClientError: If the request fails, e.g. `PermissionDeniedError`
                if the caller is not a manager of the contract's project, or
                `ConflictError` if the name is taken or a target names a data
                contract that is missing or not active.
        """
        response = self._client.post(
            f"{self._route}/contract", json=contract.to_server()
        )
        raise_from_response(response)
        return self.get(contract.name)

    def get(self, name: str) -> SubmissionContractResource:
        """Get a submission contract by name.

        Args:
            name (str): The name of the submission contract.

        Returns:
            SubmissionContractResource: The submission contract.

        Raises:
            CrossClientError: If the request fails, e.g. `ResourceNotFoundError`
                if no submission contract of that name exists.
        """
        response = self._client.get(f"{self._route}/contract/{name}")
        raise_from_response(response)
        return SubmissionContractResource.from_response(self, response.json())

    def overview(self, project_name: str) -> pd.DataFrame:
        """Get an overview of a project's submission contracts.

        Args:
            project_name (str): The name of the project.

        Returns:
            pd.DataFrame: One row per submission contract, with the columns
                `name`, `project_name`, `title`, `description`, `tags`, and
                `allow_submissions`. Empty and without columns if the project has
                no submission contracts.

        Raises:
            CrossClientError: If the request fails, e.g. `ResourceNotFoundError`
                if the project does not exist.
        """
        response = self._client.get(
            f"{self._route}/contract", params={"project_name": project_name}
        )
        raise_from_response(response)
        return pd.DataFrame(response.json()["items"])

    def delete(self, name: str) -> None:
        """Delete a submission contract by name.

        The data contracts its targets name are not affected.

        Args:
            name (str): The name of the submission contract.

        Raises:
            CrossClientError: If the request fails, e.g. `ResourceNotFoundError`
                if no submission contract of that name exists.
        """
        response = self._client.delete(f"{self._route}/contract/{name}")
        raise_from_response(response)

    def set_allow_submissions(self, name: str, allow: bool) -> None:
        """Open or close a submission contract for submissions.

        Closing it stops new uploads; submissions already received are still
        processed. Setting the value it already has changes nothing.

        Args:
            name (str): The name of the submission contract.
            allow (bool): `True` to open the submission contract, `False` to
                close it.

        Raises:
            CrossClientError: If the request fails, e.g. `PermissionDeniedError`
                if the caller is not a manager of the contract's project.
        """
        response = self._client.request(
            "PUT",
            f"{self._route}/contract/{name}/allow-submissions",
            json={"allow_submissions": allow},
        )
        raise_from_response(response)

    def get_submission(self, uuid: UUID | str) -> Submission:
        """Get a submission by its uuid, e.g. to follow its processing state.

        Args:
            uuid (UUID | str): The uuid of the submission.

        Returns:
            Submission: The submission.

        Raises:
            CrossClientError: If the request fails, e.g. `ResourceNotFoundError`
                if no submission with that uuid exists.
        """
        response = self._client.get(f"{self._route}/{uuid}")
        raise_from_response(response)
        return Submission.model_validate(response.json())

    def list_submissions(self, name: str) -> list[Submission]:
        """List every submission of a submission contract, oldest first.

        Args:
            name (str): The name of the submission contract.

        Returns:
            list[Submission]: The submissions.

        Raises:
            CrossClientError: If the request fails, e.g. `ResourceNotFoundError`
                if no submission contract of that name exists.
        """
        response = self._client.get(f"{self._route}/contract/{name}/data")
        raise_from_response(response)
        return [Submission.model_validate(item) for item in response.json()["items"]]

    def _upload_submission(
        self, name: str, file: bytes | BinaryIO, filename: str
    ) -> Submission:
        """Upload a bundle file under the given submission contract, unvalidated.

        The file is sent unchanged. The CROSS platform reads its format from
        the extension of `filename` and rejects formats it does not support.
        Use `SubmissionContractResource.submit` to validate a bundle before it
        is uploaded.

        Args:
            name (str): The name of the submission contract.
            file (bytes | BinaryIO): The content of the bundle file, as bytes or
                as a binary file object positioned at its start.
            filename (str): The name of the uploaded file,
                including its extension, e.g. `bundle.parquet`.

        Returns:
            Submission: The created submission, in state `received`.

        Raises:
            CrossClientError: If the request fails, e.g. `PermissionDeniedError`
                if the caller may not submit to the contract's project, or
                `ResourceNotFoundError` if no submission contract of that name
                exists.
        """
        response = self._client.post(
            f"{self._route}/contract/{name}/data",
            files={"file": (filename, file)},
        )
        raise_from_response(response)
        return Submission.model_validate(response.json())

__init__(client)

Initialize the service.

Parameters:

Name Type Description Default
client CrossClient

The CrossClient instance to use for API calls.

required
Source code in src/crosscontract/crossclient/services/submission/submission_service.py
def __init__(self, client: "CrossClient"):
    """Initialize the service.

    Args:
        client (CrossClient): The CrossClient instance to use for API calls.
    """
    self._client = client
    self._route = f"{self._client._base_url}{self._api_version_prefix}/submission"

create(contract)

Create a submission contract on the CROSS platform.

Every target of the contract must name an existing, active data contract. If reading it back after the creation fails, the submission contract already exists; fetch it with get instead of calling create again.

Parameters:

Name Type Description Default
contract SubmissionContract

The submission contract to create.

required

Returns:

Name Type Description
SubmissionContractResource SubmissionContractResource

The created submission contract.

Raises:

Type Description
CrossClientError

If the request fails, e.g. PermissionDeniedError if the caller is not a manager of the contract's project, or ConflictError if the name is taken or a target names a data contract that is missing or not active.

Source code in src/crosscontract/crossclient/services/submission/submission_service.py
def create(self, contract: SubmissionContract) -> SubmissionContractResource:
    """Create a submission contract on the CROSS platform.

    Every target of the contract must name an existing, active data contract.
    If reading it back after the creation fails, the submission contract
    already exists; fetch it with `get` instead of calling `create` again.

    Args:
        contract (SubmissionContract): The submission contract to create.

    Returns:
        SubmissionContractResource: The created submission contract.

    Raises:
        CrossClientError: If the request fails, e.g. `PermissionDeniedError`
            if the caller is not a manager of the contract's project, or
            `ConflictError` if the name is taken or a target names a data
            contract that is missing or not active.
    """
    response = self._client.post(
        f"{self._route}/contract", json=contract.to_server()
    )
    raise_from_response(response)
    return self.get(contract.name)

delete(name)

Delete a submission contract by name.

The data contracts its targets name are not affected.

Parameters:

Name Type Description Default
name str

The name of the submission contract.

required

Raises:

Type Description
CrossClientError

If the request fails, e.g. ResourceNotFoundError if no submission contract of that name exists.

Source code in src/crosscontract/crossclient/services/submission/submission_service.py
def delete(self, name: str) -> None:
    """Delete a submission contract by name.

    The data contracts its targets name are not affected.

    Args:
        name (str): The name of the submission contract.

    Raises:
        CrossClientError: If the request fails, e.g. `ResourceNotFoundError`
            if no submission contract of that name exists.
    """
    response = self._client.delete(f"{self._route}/contract/{name}")
    raise_from_response(response)

get(name)

Get a submission contract by name.

Parameters:

Name Type Description Default
name str

The name of the submission contract.

required

Returns:

Name Type Description
SubmissionContractResource SubmissionContractResource

The submission contract.

Raises:

Type Description
CrossClientError

If the request fails, e.g. ResourceNotFoundError if no submission contract of that name exists.

Source code in src/crosscontract/crossclient/services/submission/submission_service.py
def get(self, name: str) -> SubmissionContractResource:
    """Get a submission contract by name.

    Args:
        name (str): The name of the submission contract.

    Returns:
        SubmissionContractResource: The submission contract.

    Raises:
        CrossClientError: If the request fails, e.g. `ResourceNotFoundError`
            if no submission contract of that name exists.
    """
    response = self._client.get(f"{self._route}/contract/{name}")
    raise_from_response(response)
    return SubmissionContractResource.from_response(self, response.json())

get_submission(uuid)

Get a submission by its uuid, e.g. to follow its processing state.

Parameters:

Name Type Description Default
uuid UUID | str

The uuid of the submission.

required

Returns:

Name Type Description
Submission Submission

The submission.

Raises:

Type Description
CrossClientError

If the request fails, e.g. ResourceNotFoundError if no submission with that uuid exists.

Source code in src/crosscontract/crossclient/services/submission/submission_service.py
def get_submission(self, uuid: UUID | str) -> Submission:
    """Get a submission by its uuid, e.g. to follow its processing state.

    Args:
        uuid (UUID | str): The uuid of the submission.

    Returns:
        Submission: The submission.

    Raises:
        CrossClientError: If the request fails, e.g. `ResourceNotFoundError`
            if no submission with that uuid exists.
    """
    response = self._client.get(f"{self._route}/{uuid}")
    raise_from_response(response)
    return Submission.model_validate(response.json())

list_submissions(name)

List every submission of a submission contract, oldest first.

Parameters:

Name Type Description Default
name str

The name of the submission contract.

required

Returns:

Type Description
list[Submission]

list[Submission]: The submissions.

Raises:

Type Description
CrossClientError

If the request fails, e.g. ResourceNotFoundError if no submission contract of that name exists.

Source code in src/crosscontract/crossclient/services/submission/submission_service.py
def list_submissions(self, name: str) -> list[Submission]:
    """List every submission of a submission contract, oldest first.

    Args:
        name (str): The name of the submission contract.

    Returns:
        list[Submission]: The submissions.

    Raises:
        CrossClientError: If the request fails, e.g. `ResourceNotFoundError`
            if no submission contract of that name exists.
    """
    response = self._client.get(f"{self._route}/contract/{name}/data")
    raise_from_response(response)
    return [Submission.model_validate(item) for item in response.json()["items"]]

overview(project_name)

Get an overview of a project's submission contracts.

Parameters:

Name Type Description Default
project_name str

The name of the project.

required

Returns:

Type Description
DataFrame

pd.DataFrame: One row per submission contract, with the columns name, project_name, title, description, tags, and allow_submissions. Empty and without columns if the project has no submission contracts.

Raises:

Type Description
CrossClientError

If the request fails, e.g. ResourceNotFoundError if the project does not exist.

Source code in src/crosscontract/crossclient/services/submission/submission_service.py
def overview(self, project_name: str) -> pd.DataFrame:
    """Get an overview of a project's submission contracts.

    Args:
        project_name (str): The name of the project.

    Returns:
        pd.DataFrame: One row per submission contract, with the columns
            `name`, `project_name`, `title`, `description`, `tags`, and
            `allow_submissions`. Empty and without columns if the project has
            no submission contracts.

    Raises:
        CrossClientError: If the request fails, e.g. `ResourceNotFoundError`
            if the project does not exist.
    """
    response = self._client.get(
        f"{self._route}/contract", params={"project_name": project_name}
    )
    raise_from_response(response)
    return pd.DataFrame(response.json()["items"])

set_allow_submissions(name, allow)

Open or close a submission contract for submissions.

Closing it stops new uploads; submissions already received are still processed. Setting the value it already has changes nothing.

Parameters:

Name Type Description Default
name str

The name of the submission contract.

required
allow bool

True to open the submission contract, False to close it.

required

Raises:

Type Description
CrossClientError

If the request fails, e.g. PermissionDeniedError if the caller is not a manager of the contract's project.

Source code in src/crosscontract/crossclient/services/submission/submission_service.py
def set_allow_submissions(self, name: str, allow: bool) -> None:
    """Open or close a submission contract for submissions.

    Closing it stops new uploads; submissions already received are still
    processed. Setting the value it already has changes nothing.

    Args:
        name (str): The name of the submission contract.
        allow (bool): `True` to open the submission contract, `False` to
            close it.

    Raises:
        CrossClientError: If the request fails, e.g. `PermissionDeniedError`
            if the caller is not a manager of the contract's project.
    """
    response = self._client.request(
        "PUT",
        f"{self._route}/contract/{name}/allow-submissions",
        json={"allow_submissions": allow},
    )
    raise_from_response(response)

A handle to a submission contract that exists on the CROSS platform.

Produced by the service's create and get; not constructed directly. Its properties are read-only and change only through its methods. validate_submission checks a delivered bundle against the stored submission contract.

Attributes:

Name Type Description
name str

The name of the submission contract.

project_name str

The name of the project owning the submission contract.

title str

The title of the submission contract.

description str

The description of the submission contract.

tags list[str]

The tags of the submission contract.

allow_submissions bool

Whether the owning project may currently upload submissions.

contract SubmissionContract

The stored submission contract.

Source code in src/crosscontract/crossclient/services/submission/submission_resource.py
class SubmissionContractResource:
    """A handle to a submission contract that exists on the CROSS platform.

    Produced by the service's `create` and `get`; not constructed directly.
    Its properties are read-only and change only through its methods.
    `validate_submission` checks a delivered bundle against the stored
    submission contract.

    Attributes:
        name (str): The name of the submission contract.
        project_name (str): The name of the project owning the submission
            contract.
        title (str): The title of the submission contract.
        description (str): The description of the submission contract.
        tags (list[str]): The tags of the submission contract.
        allow_submissions (bool): Whether the owning project may currently
            upload submissions.
        contract (SubmissionContract): The stored submission contract.
    """

    def __init__(
        self,
        service: "SubmissionContractService",
        payload: SubmissionContractEntryPayload,
    ):
        """Initialize from a parsed server payload.

        Use `from_response` to build a resource from a raw response dict.

        Args:
            service (SubmissionContractService): The service the resource
                delegates its methods to.
            payload (SubmissionContractEntryPayload): The parsed server response.

        Raises:
            pydantic.ValidationError: If `payload.contract` is not a valid
                `SubmissionContract`.
        """
        self._service = service
        self._name = payload.name
        self._project_name = payload.project_name
        self._allow_submissions = payload.allow_submissions
        self._contract = SubmissionContract.model_validate(payload.contract)

    @classmethod
    def from_response(
        cls,
        service: "SubmissionContractService",
        response_json: dict[str, Any],
    ) -> "SubmissionContractResource":
        """Build a resource from a raw server response dict.

        Args:
            service (SubmissionContractService): The service the resource
                delegates its methods to.
            response_json (dict[str, Any]): The response body of a read of one
                submission contract.

        Returns:
            SubmissionContractResource: The resource.

        Raises:
            pydantic.ValidationError: If the response or its `contract` is
                malformed.
        """
        payload = SubmissionContractEntryPayload.model_validate(response_json)
        return cls(service, payload)

    @property
    def name(self) -> str:
        """Return the name of the submission contract.

        Returns:
            str: The name of the submission contract.
        """
        return self._name

    @property
    def project_name(self) -> str:
        """
        Returns:
            str: The name of the project owning the submission contract.
        """
        return self._project_name

    @property
    def title(self) -> str:
        """
        Returns:
            str: The title of the submission contract.
        """
        return self._contract.title

    @property
    def description(self) -> str:
        """
        Returns:
            str: The description of the submission contract.
        """
        return self._contract.description

    @property
    def tags(self) -> list[str]:
        """
        Returns:
            list[str]: The tags of the submission contract.
        """
        return self._contract.tags

    @property
    def allow_submissions(self) -> bool:
        """
        Returns:
            bool: Whether the owning project may currently upload submissions.
        """
        return self._allow_submissions

    @property
    def contract(self) -> SubmissionContract:
        """
        Returns:
            SubmissionContract: The stored submission contract.
        """
        return self._contract

    def __setattr__(self, name, value):
        # 1. Access the class to find the attribute definition
        # We use type(self) to avoid triggering infinite recursion or property getters
        attr = getattr(type(self), name, None)

        # 2. Check if the attribute is a property and if it has no setter
        if isinstance(attr, property) and attr.fset is None:
            raise AttributeError(
                "SubmissionContractResource is read-only. Use the methods to "
                "update properties."
            )

        # 3. If it's not a read-only property, allow the default behavior
        # This allows setting private variables like self._x = 10
        super().__setattr__(name, value)

    def __repr__(self) -> str:
        return (
            f"SubmissionContractResource(name={self.name}, "
            f"allow_submissions={self.allow_submissions})"
        )

    def set_allow_submissions(self, allow: bool) -> None:
        """Open or close the submission contract for submissions.

        Closing it stops new uploads; submissions already received are still
        processed. Setting the value it already has changes nothing.

        Args:
            allow (bool): `True` to open the submission contract, `False` to
                close it.

        Raises:
            CrossClientError: If the request fails, e.g. `PermissionDeniedError`
                if the caller is not a manager of the contract's project.
        """
        self._service.set_allow_submissions(self.name, allow)
        self._allow_submissions = allow

    def delete(self) -> None:
        """Delete the submission contract from the CROSS platform.

        The data contracts its targets name are not affected.

        Raises:
            CrossClientError: If the request fails, e.g. `ResourceNotFoundError`
                if the submission contract no longer exists.
        """
        self._service.delete(self.name)

    def refresh(self) -> None:
        """Reload the submission contract from the CROSS platform.

        Raises:
            CrossClientError: If the request fails, e.g. `ResourceNotFoundError`
                if the submission contract no longer exists.
            ValueError: If the CROSS platform returns a submission contract of a
                different name.
        """
        remote = self._service.get(self.name)
        if remote.name != self.name:
            raise ValueError(
                f"Fetched contract name '{remote.name}' does not match "
                f"resource name '{self.name}'."
            )
        self._project_name = remote.project_name
        self._allow_submissions = remote.allow_submissions
        self._contract = remote.contract

    def submissions(self) -> list[Submission]:
        """Fetch every submission of this submission contract, oldest first.

        Returns:
            list[Submission]: The submissions.

        Raises:
            CrossClientError: If the request fails, e.g. `ResourceNotFoundError`
                if the submission contract no longer exists.
        """
        return self._service.list_submissions(self.name)

    def validate_submission(
        self,
        df: pd.DataFrame,
        check_existing_primary_key: bool = False,
        check_existing_foreign_key: bool = True,
        check_dimension_granularity: bool = True,
        lazy: bool = True,
    ) -> dict[str, pd.DataFrame]:
        """Validate a delivered bundle against this submission contract.

        Runs `SubmissionHandler.validate_submission` on the stored contract,
        reading the target contracts and their stored values from the CROSS
        platform. The steps stop at the first failure; see that method for what
        each check covers. By default the stored primary keys are not checked,
        because a submission replaces the rows stored under its `replace_key`.
        Validation here is advisory: the platform validates a submission again
        on ingest and can still reject it.

        Args:
            df (pd.DataFrame): The bundle as delivered.
            check_existing_primary_key (bool): If True, also check each target's
                primary key against the values already stored for the contract
                it names, including the rows this submission would replace.
                Defaults to False.
            check_existing_foreign_key (bool): If True, also check each target's
                foreign keys against the values already stored for the contracts
                they reference. Defaults to True.
            check_dimension_granularity (bool): If True, also check that no
                ValueVariable target reports a dimension member alongside one of
                its descendants. Defaults to True.
            lazy (bool): If True, collect all of a step's validation errors and
                raise them together. If False, raise the first error
                encountered. Defaults to True.

        Returns:
            dict[str, pd.DataFrame]: The validated, coerced target data, keyed
                by target name.

        Raises:
            SchemaValidationError: The bundle does not satisfy the submission
                contract's own schema.
            UnclaimedRowsError: The bundle holds rows no target claims.
            TargetValidationError: At least one target's data failed, holding
                one entry per failing target.
            ValueError: A target names a contract that cannot be read.
            KeyError: A column named by a target's `filters` is absent from the
                bundle.
            CrossClientError: A read from the CROSS platform failed.
        """
        resolver = CrossContractResolver(self._service._client.contracts)
        return SubmissionHandler(self.contract, df).validate_submission(
            resolver=resolver,
            check_existing_primary_key=check_existing_primary_key,
            check_existing_foreign_key=check_existing_foreign_key,
            check_dimension_granularity=check_dimension_granularity,
            lazy=lazy,
        )

    def submit(
        self, df: pd.DataFrame, filename: str | None = None, validate: bool = True
    ) -> Submission:
        """Validate a delivered bundle, then upload it under this submission contract.

        The bundle is uploaded as a parquet file, exactly as handed in. With
        `validate=True` it is first checked by `validate_submission` with its
        defaults, and nothing is uploaded if any check fails. To validate with
        other settings, call `validate_submission` with the flags you need and
        then `submit(df, validate=False)`.

        Validation here is advisory: the CROSS platform validates the
        submission again on ingest and can still reject it. Processing on the
        platform is asynchronous; follow the returned submission's state with
        `SubmissionContractService.get_submission`.

        Args:
            df (pd.DataFrame): The bundle as delivered.
            filename (str | None, optional): The stem of the uploaded file;
                `.parquet` is appended. Defaults to `None`, which names the file
                after the submission contract and the UTC time of the upload,
                e.g. `my_contract_20261017124301.parquet`.
            validate (bool, optional): Whether to validate the bundle before
                uploading it. Defaults to `True`.

        Returns:
            Submission: The created submission, in state `received`.

        Raises:
            SchemaValidationError: The bundle does not satisfy the submission
                contract's own schema. Only with `validate=True`.
            UnclaimedRowsError: The bundle holds rows no target claims. Only
                with `validate=True`.
            TargetValidationError: At least one target's data failed. Only with
                `validate=True`.
            ValueError: A target names a contract that cannot be read. Only
                with `validate=True`.
            KeyError: A column named by a target's `filters` is absent from the
                bundle. Only with `validate=True`.
            pyarrow.ArrowException: The bundle cannot be written as parquet,
                e.g. `ArrowTypeError` or `ArrowInvalid` for a column mixing
                strings and integers. Can occur even when validation passes;
                cast the column to one type before submitting.
            CrossClientError: A request to the CROSS platform failed, e.g.
                `PermissionDeniedError` if the caller may not submit to the
                contract's project.
        """
        if filename is None:
            filename = f"{self.name}_{datetime.now(tz=UTC):%Y%m%d%H%M%S}"
        if validate:
            self.validate_submission(df)
        buffer = BytesIO()
        df.to_parquet(buffer, index=False)
        buffer.seek(0)
        return self._service._upload_submission(
            self.name, buffer, f"{filename}.parquet"
        )

allow_submissions property

Returns:

Name Type Description
bool bool

Whether the owning project may currently upload submissions.

contract property

Returns:

Name Type Description
SubmissionContract SubmissionContract

The stored submission contract.

description property

Returns:

Name Type Description
str str

The description of the submission contract.

name property

Return the name of the submission contract.

Returns:

Name Type Description
str str

The name of the submission contract.

project_name property

Returns:

Name Type Description
str str

The name of the project owning the submission contract.

tags property

Returns:

Type Description
list[str]

list[str]: The tags of the submission contract.

title property

Returns:

Name Type Description
str str

The title of the submission contract.

__init__(service, payload)

Initialize from a parsed server payload.

Use from_response to build a resource from a raw response dict.

Parameters:

Name Type Description Default
service SubmissionContractService

The service the resource delegates its methods to.

required
payload SubmissionContractEntryPayload

The parsed server response.

required

Raises:

Type Description
ValidationError

If payload.contract is not a valid SubmissionContract.

Source code in src/crosscontract/crossclient/services/submission/submission_resource.py
def __init__(
    self,
    service: "SubmissionContractService",
    payload: SubmissionContractEntryPayload,
):
    """Initialize from a parsed server payload.

    Use `from_response` to build a resource from a raw response dict.

    Args:
        service (SubmissionContractService): The service the resource
            delegates its methods to.
        payload (SubmissionContractEntryPayload): The parsed server response.

    Raises:
        pydantic.ValidationError: If `payload.contract` is not a valid
            `SubmissionContract`.
    """
    self._service = service
    self._name = payload.name
    self._project_name = payload.project_name
    self._allow_submissions = payload.allow_submissions
    self._contract = SubmissionContract.model_validate(payload.contract)

delete()

Delete the submission contract from the CROSS platform.

The data contracts its targets name are not affected.

Raises:

Type Description
CrossClientError

If the request fails, e.g. ResourceNotFoundError if the submission contract no longer exists.

Source code in src/crosscontract/crossclient/services/submission/submission_resource.py
def delete(self) -> None:
    """Delete the submission contract from the CROSS platform.

    The data contracts its targets name are not affected.

    Raises:
        CrossClientError: If the request fails, e.g. `ResourceNotFoundError`
            if the submission contract no longer exists.
    """
    self._service.delete(self.name)

from_response(service, response_json) classmethod

Build a resource from a raw server response dict.

Parameters:

Name Type Description Default
service SubmissionContractService

The service the resource delegates its methods to.

required
response_json dict[str, Any]

The response body of a read of one submission contract.

required

Returns:

Name Type Description
SubmissionContractResource SubmissionContractResource

The resource.

Raises:

Type Description
ValidationError

If the response or its contract is malformed.

Source code in src/crosscontract/crossclient/services/submission/submission_resource.py
@classmethod
def from_response(
    cls,
    service: "SubmissionContractService",
    response_json: dict[str, Any],
) -> "SubmissionContractResource":
    """Build a resource from a raw server response dict.

    Args:
        service (SubmissionContractService): The service the resource
            delegates its methods to.
        response_json (dict[str, Any]): The response body of a read of one
            submission contract.

    Returns:
        SubmissionContractResource: The resource.

    Raises:
        pydantic.ValidationError: If the response or its `contract` is
            malformed.
    """
    payload = SubmissionContractEntryPayload.model_validate(response_json)
    return cls(service, payload)

refresh()

Reload the submission contract from the CROSS platform.

Raises:

Type Description
CrossClientError

If the request fails, e.g. ResourceNotFoundError if the submission contract no longer exists.

ValueError

If the CROSS platform returns a submission contract of a different name.

Source code in src/crosscontract/crossclient/services/submission/submission_resource.py
def refresh(self) -> None:
    """Reload the submission contract from the CROSS platform.

    Raises:
        CrossClientError: If the request fails, e.g. `ResourceNotFoundError`
            if the submission contract no longer exists.
        ValueError: If the CROSS platform returns a submission contract of a
            different name.
    """
    remote = self._service.get(self.name)
    if remote.name != self.name:
        raise ValueError(
            f"Fetched contract name '{remote.name}' does not match "
            f"resource name '{self.name}'."
        )
    self._project_name = remote.project_name
    self._allow_submissions = remote.allow_submissions
    self._contract = remote.contract

set_allow_submissions(allow)

Open or close the submission contract for submissions.

Closing it stops new uploads; submissions already received are still processed. Setting the value it already has changes nothing.

Parameters:

Name Type Description Default
allow bool

True to open the submission contract, False to close it.

required

Raises:

Type Description
CrossClientError

If the request fails, e.g. PermissionDeniedError if the caller is not a manager of the contract's project.

Source code in src/crosscontract/crossclient/services/submission/submission_resource.py
def set_allow_submissions(self, allow: bool) -> None:
    """Open or close the submission contract for submissions.

    Closing it stops new uploads; submissions already received are still
    processed. Setting the value it already has changes nothing.

    Args:
        allow (bool): `True` to open the submission contract, `False` to
            close it.

    Raises:
        CrossClientError: If the request fails, e.g. `PermissionDeniedError`
            if the caller is not a manager of the contract's project.
    """
    self._service.set_allow_submissions(self.name, allow)
    self._allow_submissions = allow

submissions()

Fetch every submission of this submission contract, oldest first.

Returns:

Type Description
list[Submission]

list[Submission]: The submissions.

Raises:

Type Description
CrossClientError

If the request fails, e.g. ResourceNotFoundError if the submission contract no longer exists.

Source code in src/crosscontract/crossclient/services/submission/submission_resource.py
def submissions(self) -> list[Submission]:
    """Fetch every submission of this submission contract, oldest first.

    Returns:
        list[Submission]: The submissions.

    Raises:
        CrossClientError: If the request fails, e.g. `ResourceNotFoundError`
            if the submission contract no longer exists.
    """
    return self._service.list_submissions(self.name)

submit(df, filename=None, validate=True)

Validate a delivered bundle, then upload it under this submission contract.

The bundle is uploaded as a parquet file, exactly as handed in. With validate=True it is first checked by validate_submission with its defaults, and nothing is uploaded if any check fails. To validate with other settings, call validate_submission with the flags you need and then submit(df, validate=False).

Validation here is advisory: the CROSS platform validates the submission again on ingest and can still reject it. Processing on the platform is asynchronous; follow the returned submission's state with SubmissionContractService.get_submission.

Parameters:

Name Type Description Default
df DataFrame

The bundle as delivered.

required
filename str | None

The stem of the uploaded file; .parquet is appended. Defaults to None, which names the file after the submission contract and the UTC time of the upload, e.g. my_contract_20261017124301.parquet.

None
validate bool

Whether to validate the bundle before uploading it. Defaults to True.

True

Returns:

Name Type Description
Submission Submission

The created submission, in state received.

Raises:

Type Description
SchemaValidationError

The bundle does not satisfy the submission contract's own schema. Only with validate=True.

UnclaimedRowsError

The bundle holds rows no target claims. Only with validate=True.

TargetValidationError

At least one target's data failed. Only with validate=True.

ValueError

A target names a contract that cannot be read. Only with validate=True.

KeyError

A column named by a target's filters is absent from the bundle. Only with validate=True.

ArrowException

The bundle cannot be written as parquet, e.g. ArrowTypeError or ArrowInvalid for a column mixing strings and integers. Can occur even when validation passes; cast the column to one type before submitting.

CrossClientError

A request to the CROSS platform failed, e.g. PermissionDeniedError if the caller may not submit to the contract's project.

Source code in src/crosscontract/crossclient/services/submission/submission_resource.py
def submit(
    self, df: pd.DataFrame, filename: str | None = None, validate: bool = True
) -> Submission:
    """Validate a delivered bundle, then upload it under this submission contract.

    The bundle is uploaded as a parquet file, exactly as handed in. With
    `validate=True` it is first checked by `validate_submission` with its
    defaults, and nothing is uploaded if any check fails. To validate with
    other settings, call `validate_submission` with the flags you need and
    then `submit(df, validate=False)`.

    Validation here is advisory: the CROSS platform validates the
    submission again on ingest and can still reject it. Processing on the
    platform is asynchronous; follow the returned submission's state with
    `SubmissionContractService.get_submission`.

    Args:
        df (pd.DataFrame): The bundle as delivered.
        filename (str | None, optional): The stem of the uploaded file;
            `.parquet` is appended. Defaults to `None`, which names the file
            after the submission contract and the UTC time of the upload,
            e.g. `my_contract_20261017124301.parquet`.
        validate (bool, optional): Whether to validate the bundle before
            uploading it. Defaults to `True`.

    Returns:
        Submission: The created submission, in state `received`.

    Raises:
        SchemaValidationError: The bundle does not satisfy the submission
            contract's own schema. Only with `validate=True`.
        UnclaimedRowsError: The bundle holds rows no target claims. Only
            with `validate=True`.
        TargetValidationError: At least one target's data failed. Only with
            `validate=True`.
        ValueError: A target names a contract that cannot be read. Only
            with `validate=True`.
        KeyError: A column named by a target's `filters` is absent from the
            bundle. Only with `validate=True`.
        pyarrow.ArrowException: The bundle cannot be written as parquet,
            e.g. `ArrowTypeError` or `ArrowInvalid` for a column mixing
            strings and integers. Can occur even when validation passes;
            cast the column to one type before submitting.
        CrossClientError: A request to the CROSS platform failed, e.g.
            `PermissionDeniedError` if the caller may not submit to the
            contract's project.
    """
    if filename is None:
        filename = f"{self.name}_{datetime.now(tz=UTC):%Y%m%d%H%M%S}"
    if validate:
        self.validate_submission(df)
    buffer = BytesIO()
    df.to_parquet(buffer, index=False)
    buffer.seek(0)
    return self._service._upload_submission(
        self.name, buffer, f"{filename}.parquet"
    )

validate_submission(df, check_existing_primary_key=False, check_existing_foreign_key=True, check_dimension_granularity=True, lazy=True)

Validate a delivered bundle against this submission contract.

Runs SubmissionHandler.validate_submission on the stored contract, reading the target contracts and their stored values from the CROSS platform. The steps stop at the first failure; see that method for what each check covers. By default the stored primary keys are not checked, because a submission replaces the rows stored under its replace_key. Validation here is advisory: the platform validates a submission again on ingest and can still reject it.

Parameters:

Name Type Description Default
df DataFrame

The bundle as delivered.

required
check_existing_primary_key bool

If True, also check each target's primary key against the values already stored for the contract it names, including the rows this submission would replace. Defaults to False.

False
check_existing_foreign_key bool

If True, also check each target's foreign keys against the values already stored for the contracts they reference. Defaults to True.

True
check_dimension_granularity bool

If True, also check that no ValueVariable target reports a dimension member alongside one of its descendants. Defaults to True.

True
lazy bool

If True, collect all of a step's validation errors and raise them together. If False, raise the first error encountered. Defaults to True.

True

Returns:

Type Description
dict[str, DataFrame]

dict[str, pd.DataFrame]: The validated, coerced target data, keyed by target name.

Raises:

Type Description
SchemaValidationError

The bundle does not satisfy the submission contract's own schema.

UnclaimedRowsError

The bundle holds rows no target claims.

TargetValidationError

At least one target's data failed, holding one entry per failing target.

ValueError

A target names a contract that cannot be read.

KeyError

A column named by a target's filters is absent from the bundle.

CrossClientError

A read from the CROSS platform failed.

Source code in src/crosscontract/crossclient/services/submission/submission_resource.py
def validate_submission(
    self,
    df: pd.DataFrame,
    check_existing_primary_key: bool = False,
    check_existing_foreign_key: bool = True,
    check_dimension_granularity: bool = True,
    lazy: bool = True,
) -> dict[str, pd.DataFrame]:
    """Validate a delivered bundle against this submission contract.

    Runs `SubmissionHandler.validate_submission` on the stored contract,
    reading the target contracts and their stored values from the CROSS
    platform. The steps stop at the first failure; see that method for what
    each check covers. By default the stored primary keys are not checked,
    because a submission replaces the rows stored under its `replace_key`.
    Validation here is advisory: the platform validates a submission again
    on ingest and can still reject it.

    Args:
        df (pd.DataFrame): The bundle as delivered.
        check_existing_primary_key (bool): If True, also check each target's
            primary key against the values already stored for the contract
            it names, including the rows this submission would replace.
            Defaults to False.
        check_existing_foreign_key (bool): If True, also check each target's
            foreign keys against the values already stored for the contracts
            they reference. Defaults to True.
        check_dimension_granularity (bool): If True, also check that no
            ValueVariable target reports a dimension member alongside one of
            its descendants. Defaults to True.
        lazy (bool): If True, collect all of a step's validation errors and
            raise them together. If False, raise the first error
            encountered. Defaults to True.

    Returns:
        dict[str, pd.DataFrame]: The validated, coerced target data, keyed
            by target name.

    Raises:
        SchemaValidationError: The bundle does not satisfy the submission
            contract's own schema.
        UnclaimedRowsError: The bundle holds rows no target claims.
        TargetValidationError: At least one target's data failed, holding
            one entry per failing target.
        ValueError: A target names a contract that cannot be read.
        KeyError: A column named by a target's `filters` is absent from the
            bundle.
        CrossClientError: A read from the CROSS platform failed.
    """
    resolver = CrossContractResolver(self._service._client.contracts)
    return SubmissionHandler(self.contract, df).validate_submission(
        resolver=resolver,
        check_existing_primary_key=check_existing_primary_key,
        check_existing_foreign_key=check_existing_foreign_key,
        check_dimension_granularity=check_dimension_granularity,
        lazy=lazy,
    )

Bases: BaseModel

A submission on the CROSS platform: what was uploaded, by whom, and its state.

Holds metadata only, not the uploaded file. state is received right after the upload, processing while the platform works on it, and ends as current (valid and stored), rejected (invalid, nothing stored) or failed (a platform error, nothing stored). A current submission becomes superseded once a newer submission replaces its data.

Source code in src/crosscontract/crossclient/services/submission/schemas.py
class Submission(BaseModel):
    """A submission on the CROSS platform: what was uploaded, by whom, and its state.

    Holds metadata only, not the uploaded file. `state` is `received` right
    after the upload, `processing` while the platform works on it, and ends as
    `current` (valid and stored), `rejected` (invalid, nothing stored) or
    `failed` (a platform error, nothing stored). A `current` submission becomes
    `superseded` once a newer submission replaces its data.
    """

    uuid: UUID = Field(..., description="The public identifier of the submission")
    submission_contract_name: str = Field(
        ...,
        description="The name of the submission contract the bundle was submitted for",
    )
    project_name: str = Field(
        ...,
        description="The name of the project owning the submission contract",
    )
    submitted_by: str = Field(
        ...,
        description="The username of the submitter",
    )
    original_filename: str = Field(
        ..., description="The file name as sent by the client"
    )
    size: int = Field(..., description="The size of the uploaded file in bytes")
    hash: str = Field(..., description="The SHA-256 hex digest of the uploaded file")
    state: SubmissionState = Field(
        ..., description="The processing state of the submission"
    )
    created_at: datetime = Field(..., description="When the submission was received")