Version

A resource version represents the exact state of a resource at a given point in time. The resource is responsible for defining what a version actually is, and this is defined with Concourse Tools using a Version subclass.

More information on versions can be found in the Concourse documentation.

Version Parent Class

class concoursetools.version.Version[source]

A simple wrapper around a Concourse version.

Users should inherit from this class when defining the version schema for their resource. The class is then used by the resource to parse the input and generate the output for the relevant Concourse steps.

Tip

It is usually recommended to inherit from TypedVersion instead, to reduce code and enable some more useful type conversion features.

Example:

If your resource is tracking git commits, then the version might only consist of a git hash corresponding to that commit:

class GitCommitVersion(Version):

    def __init__(self, commit_hash):
        self.commit_hash = commit_hash

By default, this will correspond to a JSON object which looks like this:

{
    "commit_hash": "abcdef..."
}

To change this behaviour, overload the to_flat_dict() and from_flat_dict() methods in the class.

to_flat_dict()[source]

Convert the instance to a dictionary with string fields.

Important

Concourse requires the keys and values of this dictionary to be strings. This is subtly enforced by Concourse Tools as both keys and values are cast to strings before the output stage, but you should not rely on this behaviour for any types other than str.

By default, this method outputs a dictionary of public attributes, with each value cast to a string. This is then converted to JSON by the resource.

Return type:

dict[str, str]

Returns:

A key/value dictionary representing the version.

Example:

Suppose that you wanted to include date/time information in your version for easier comparisons. You could simply add a timestamp, but it’s more Pythonic to use datetime. In which case, you need to properly convert to and from some flat representation of the date:

class GitCommitVersion(Version):

    def __init__(self, commit_hash, date):
        self.commit_hash = commit_hash
        self.date = date

    def to_flat_dict(self):
        return {
            "commit_hash": self.commit_hash,
            "timestamp": str(int(self.date.timestamp())),
        }
classmethod from_flat_dict(version_dict)[source]

Load an instance from a dictionary representing the version.

By default, this method feeds the contents of the dictionary directly to the class initialiser. This assumes that all initialisation parameters are strings.

Caution

Simple types such as int and bool will be cast to strings automatically by the default behaviour of to_flat_dict(), but the reverse will not occur automatically.

Parameters:

version_dict (dict[str, str]) – A string-only key/value dictionary representing the version.

Example:

A user may wish to include whether or not the commit is a “merge commit” within the version. However, bool("False") will still return True. Instead, the user should do something like this:

class GitCommitVersion(Version):

    def __init__(self, commit_hash, is_merge):
        self.commit_hash = commit_hash
        self.is_merge = is_merge

    @classmethod
    def from_flat_dict(cls, version_dict):
        is_merge = (version_dict["is_merge"] == "True")
        return cls(version_dict["commit_hash"], is_merge)
Return type:

Version

Tip

It is often useful to overload this method to deal with types such as set, Enum and Path. However, it is also beneficial to instead inherit from TypedVersion instead.

Typed Version Class

class concoursetools.version.TypedVersion[source]

A Version subclass with automatic type flattening and un-flattening.

Rewriting the logic for flattening and un-flattening version attributes across multiple resource types is frustrating, and results in a lot of boring code. This class allows the user to specify functions for flattening and un-flattening various types, which are then called automatically by to_flat_dict() and from_flat_dict(). These are registered using the flatten() and un_flatten() decorators respectively.

Note

This requires the dataclasses.dataclass() decorator to work.

Example:
>>> from dataclasses import dataclass
>>> from datetime import datetime
...
>>> @dataclass
... class GitCommitVersion(TypedVersion):
...     commit_hash: str
...     date: datetime
...
>>> version = GitCommitVersion("abcdef", datetime(2020, 1, 1, 12, 30))
>>> version.to_flat_dict()
{'commit_hash': 'abcdef', 'date': '1577881800'}

Caution

The full MRO of each object is looked up when calling the flatten and un-flatten functions, so any type which is a subclass of a registered type will still call the same functions, unless explicitly overwritten.

classmethod flatten(func)[source]

Register a function for flattening a specific type.

Parameters:

func (Callable[[TypeVar(T)], str]) – A function taking a single object of the given type, and returning a string.

Return type:

Callable[[TypeVar(T)], str]

Warning

The decorated function must have an input type hint to be registered properly.

Example:
>>> from datetime import datetime
...
>>> @TypedVersion.flatten
... def _(obj: datetime) -> str:
...     return str(int(obj.timestamp()))
classmethod un_flatten(func)[source]

Register a function for un-flattening a string to a specific type.

Parameters:

func (Callable[[type[TypeVar(T)], str], TypeVar(T)]) – A function taking a a destination type and a flattened string, and returning an instance of that type.

Return type:

Callable[[type[TypeVar(T)], str], TypeVar(T)]

Warning

The decorated function must have a valid return type to be registered properly.

Example:
>>> from datetime import datetime
...
>>> @TypedVersion.un_flatten
... def _(type_: Type[datetime], obj: str) -> datetime:
...     return type_.fromtimestamp(int(obj))

Supported Types

Concourse Tools has out-of-the-box support for a few common types:

  • datetime: The datetime object is mapped to an integer timestamp.

  • bool: The boolean is mapped to the strings "True" or "False".

  • Enum: Enums are mapped to their names, so MyEnum.ONE maps to ONE.

  • Path: Paths are mapped to their string representations.

Version Comparisons

It is often beneficial to directly compare version within your code.

Hashing

By default, every version is hashable, and this hash() is determined by the version class and the output of to_flat_dict(). Key/value pairs are sorted and then hashed as a tuple, which is combined with the hash of the class.

Equality

By default, two versions are equal if they have the same hash(). However, you may wish to overload this. For example, consider the following version class:

class GitBranch(Version):

    def __init__(self, branch_name: str):
        self.branch_name = branch_name

Under the default behaviour, GitBranch("main") and GitBranch("origin/main") will be not be considered equal, but this might not be ideal. The fastest way to fix this is to overload __eq__() like so:

class GitBranch(Version):

    def __init__(self, branch_name: str):
        self.branch_name = branch_name

    def __eq__(self, other):
        return self.branch_name.split("/")[-1] == other.branch_name.split("/")[-1]

Ordering

Sometimes you need to order versions to simplify your scripts (returning the latest version, etc.), but by default versions are not comparable in this way. This can be fixed by also inheriting from SortableVersionMixin, which expects you to implement __lt__(). We want version_a <= version_b if and only if version_a is no older than version_b.

class concoursetools.version.SortableVersionMixin[source]

A mixin for Version subclasses which allows comparisons between instances.

Note

Once __lt__() has been implemented, you will be able to use <, >, <= and >= with your version classes.

Example:
>>> import datetime
>>>
>>> class MyVersion(Version, SortableVersionMixin):
...
...     def __init__(self, commit_hash, date: datetime.datetime):
...         self.commit_hash = commit_hash
...         self.date = date
...
...     def __lt__(self, other: "MyVersion"):
...         try:
...             return self.date < other.date
...         except AttributeError:
...             return NotImplemented

Multi Versions

class concoursetools.additional.MultiVersion(versions)[source]

Wraps multiple versions into a single class.

Caution

Users shouldn’t invoke this version outside of the MultiVersionConcourseResource class.

Parameters:

versions (set[Version]) – A (specifically unordered) collection of subversions to be contained in a single version.

Tip

Two multi-versions are equal if their respective set of subversions are also equal.

property key: str

Return the key used for the JSON encoded version data.

property sub_version_class: type[SortableVersionT]

Return the class used to parse the subversions.

property sub_version_data: list[dict[str, str]]

Return a list of flattened subversions.

This is created by calling to_flat_dict() on each subversion.

to_flat_dict()[source]

Convert the instance to a dictionary with string fields.

The resulting version has a single key/value pair, mapping key to the sub_version_data as a JSON-encoded string.

Return type:

dict[str, str]

classmethod from_flat_dict(version_dict)[source]

Load an instance from a dictionary representing the version.

This works by extracting the key (given by key) from the mapping, decoding the corresponding JSON blob and loading each sub-configuration using the sub_version_class.

Parameters:

version_dict (dict[str, str]) – A string-only key/value dictionary representing the multi-version.

Return type:

MultiVersion[Version]