pycroft.helpers

This package contains certain parts of the business logic which do not depend on our model.

This can include logic concerning ip addresses, intervals, internationalization, etc.

pycroft.helpers.date

diff_month(d1: date, d2: date) int[source]

Calculate the difference in months ignoring the days

If d1 > d2, the result is positive.

last_day_of_month(d: date) date[source]

pycroft.helpers.errorcode

digits(n: int, base: int = 10) Iterator[int][source]

Generate all digits of number in a given base starting with the least significant digit.

Parameters
  • n – An integral number

  • base – Defaults to 10.

class ErrorCode[source]

Error detection code abstract base class.

Subclasses must implement at least the calculate method.

abstract calculate(number: int) int[source]
is_valid(number: int, code: int) bool[source]

Validates a (number, code) pair by calculating the code and comparing.

class DigitSumModNCode(mod: int)[source]

Digit sum mod-n error detection code. Does not catch digit transposition errors.

calculate(number: int) int[source]
class Mod97Code[source]

Expand a number on the right so that its mod-97 is 1. This is a more advanced error detection code based on the IBAN check digits scheme.

calculate(number: int) int[source]
is_valid(number: int, code: int) bool[source]

Validates a (number, code) pair by calculating the code and comparing.

pycroft.helpers.i18n

get_locale() Locale[source]
set_translation_lookup(lookup_func: Callable[[], Translations]) None[source]
class Message(domain: str | None = None)[source]
classmethod from_json(json_string: str) Message[source]
domain
args: Iterable[None | bool | str | int | float | Decimal | Money | date | datetime | time | timedelta | Interval]
kwargs: dict[str, None | bool | str | int | float | Decimal | Money | date | datetime | time | timedelta | Interval]
to_json() str[source]
format(*args: None | bool | str | int | float | Decimal | Money | date | datetime | time | timedelta | Interval, **kwargs: None | bool | str | int | float | Decimal | Money | date | datetime | time | timedelta | Interval) Self[source]
localize(options: dict[type, dict[str, Any]] = None) str[source]
localized(json_string: str, options: dict[type, dict[str, Any]] | None = None) str[source]
gettext(message: str) str[source]
dngettext(domain: str, singular: str, plural: str, n: int) str[source]
ngettext(singular: str, plural: str, n: int) str[source]
dgettext(domain: str, message: str) str[source]
deferred_gettext(message) SimpleMessage[source]
deferred_dngettext(domain, singular, plural, n) NumericalMessage[source]
deferred_ngettext(singular, plural, n) NumericalMessage[source]
deferred_dgettext(domain: str, message: str) SimpleMessage[source]

pycroft.helpers.i18n.babel

get_locale() Locale[source]
get_translations() Translations[source]
set_locale_lookup(lookup_func: Callable[[], Locale]) None[source]
set_translation_lookup(lookup_func: Callable[[], Translations]) None[source]
gettext(message: str) str[source]
dgettext(domain: str, message: str) str[source]
ngettext(singular: str, plural: str, n: int) str[source]
dngettext(domain: str, singular: str, plural: str, n: int) str[source]

pycroft.helpers.i18n.deferred

deferred_gettext(message) SimpleMessage[source]
deferred_dgettext(domain: str, message: str) SimpleMessage[source]
deferred_ngettext(singular, plural, n) NumericalMessage[source]
deferred_dngettext(domain, singular, plural, n) NumericalMessage[source]

pycroft.helpers.i18n.formatting

class Formattable(*args, **kwargs)[source]
class Formatter(*args, **kwargs)[source]
format_number(n, insert_commas=True)[source]
format_decimal(d, format=None)[source]
format_currency(money: Money, format=None)[source]
format_date(d, format='medium')[source]
format_datetime(d, format='medium', tzinfo=None)[source]
format_time(t, format='medium', tzinfo=None)[source]
format_timedelta(delta, granularity='second', threshold=0.85, add_direction=False, format='medium')[source]
format_bool(v)[source]
format_none(n)[source]
format_interval(interval: Interval, **options: dict[str, Any])[source]
identity(x)[source]
format_param(p, options: dict[type, dict[str, Any]]) Formattable[source]

pycroft.helpers.i18n.message

class Message(domain: str | None = None)[source]
classmethod from_json(json_string: str) Message[source]
domain
args: Iterable[None | bool | str | int | float | Decimal | Money | date | datetime | time | timedelta | Interval]
kwargs: dict[str, None | bool | str | int | float | Decimal | Money | date | datetime | time | timedelta | Interval]
to_json() str[source]
format(*args: None | bool | str | int | float | Decimal | Money | date | datetime | time | timedelta | Interval, **kwargs: None | bool | str | int | float | Decimal | Money | date | datetime | time | timedelta | Interval) Self[source]
localize(options: dict[type, dict[str, Any]] = None) str[source]
class ErroneousMessage(text)[source]
domain
args: Iterable[None | bool | str | int | float | Decimal | Money | date | datetime | time | timedelta | Interval]
kwargs: dict[str, None | bool | str | int | float | Decimal | Money | date | datetime | time | timedelta | Interval]
class SimpleMessage(message, domain=None)[source]
message
class NumericalMessage(singular, plural, n, domain=None)[source]
singular
plural
n

pycroft.helpers.i18n.options

pycroft.helpers.i18n.serde

identity(x)[source]
deserialize_money(v)[source]
serialize_interval(interval: Interval) dict[str, Any][source]
deserialize_interval(value: dict[str, Any]) Interval[source]
serialize_param(param)[source]
deserialize_param(param)[source]

pycroft.helpers.i18n.types

namedtuple Money(value, currency)

Money(value, currency)

Fields
  1.  value – Alias for field number 0

  2.  currency – Alias for field number 1

pycroft.helpers.i18n.utils

qualified_typename(type_: type) str[source]

pycroft.helpers.interval

PositiveInfinity = pycroft.helpers.interval.PositiveInfinity

+∞

class PositiveInfinityType[source]

dummy placeholder type to work around mypy limitations

NegativeInfinity = pycroft.helpers.interval.NegativeInfinity

-∞

class NegativeInfinityType[source]

dummy placeholder type to work around mypy limitations

class Bound(value: TWithInfinity, is_closed: bool)[source]

Represents a bound of an interval, i.e. value + closedness.

The value is either:

property value: TWithInfinity
property closed: bool
property pg_identifier: str
property unbounded: bool
class Interval(lower_bound: Bound[T], upper_bound: Bound[T])[source]

Represents an bounded or unbounded interval.

Bounds may be any comparable types. If lengths should be calculated, bounds must also implement subtraction and the return type of the subtraction must implement addition.

If a bound is None it means unbound, so a None begin is conceptually negative infinity and a None end positive infinity.

This class is immutable.

Intervals are ordered lexicographically according to begin and end per default.

It implements the relations from Allen’s interval algebra, i.e. before, during, overlaps, starts, finishes and equals.

Todo

replace by generic namedtuple once we’re on Python 3.11 for nicer typing behavior

classmethod from_explicit_data(lower: T | None, lower_closed: bool, upper: T | None, upper_closed: bool) t.Self[source]
property lower_bound: Bound[T]
Returns

The lower bound object

property upper_bound: Bound[T]
Returns

The upper bound object

property begin: T | None
property end: T | None
property unbounded: bool
property empty: bool

Tests whether the interval is empty

property length: Any

Compute the interval’s length

Returns

None if the interval is unbounded else end - begin

strictly_before(other: Interval[T]) bool[source]

Tests if this interval is strictly before another interval.

An interval is strictly before another if its end is less than the other’s begin.

Note

This is not the same as self < other.

before(other: Interval[T]) bool[source]

Tests if this interval is before another interval.

An interval is before another if its end is less than or equal to the other’s begin.

Note

This is not the same as self <= other.

strictly_after(other: Interval[T]) bool[source]

Tests if this interval is strictly after another interval.

An interval is strictly after another if its begin is greater than the other’s end.

Note

This is not the same as self > other.

after(other: Interval[T]) bool[source]

Tests if this interval is after another interval.

An interval is after another if its begin is greater than or equal to the other’s end.

Note

This is not the same as self >= other.

meets(other: Interval[T]) bool[source]

Tests if this interval meets another interval.

Two intervals meet if the end of the first interval is equal to the begin of the second interval and at least one of the bounds is closed. This means that the intervals do not necessarily have to overlap.

strictly_overlaps(other: Interval[T]) bool[source]

Tests if this interval overlaps strictly with another interval.

Two intervals overlap if each begin is strictly before the other’s end. This means that the intervals may not be equal.

overlaps(other: Interval[T]) bool[source]

Tests if this interval overlaps with another interval.

Two intervals overlap if each begin is before the other’s end.

strictly_during(other: Interval[T]) bool[source]

Tests if this interval is strictly during (strictly contained in) another interval.

An interval is strictly during another if its begin is greater than the other’s and its end is less than the other’s.

during(other: Interval[T]) bool[source]

Tests if this interval is during (contained in) another interval.

An interval is during another if its begin is greather than or equal to the other’s and its end is less than or equal to the other’s.

strictly_contains(other: Interval[T]) bool[source]

Tests if this interval strictly contains another interval.

An interval strictly contains another if its begin is less than the other’s and its end is greater than the other’s.

contains(other: Interval[T]) bool[source]

Tests if this interval contains another interval.

An interval contains another if its begin is less than or equal to the other’s and its end greater than or equal to the other’s.

starts(other: Interval[T]) bool[source]

Tests whether both intervals start at the same value.

finishes(other: Interval[T]) bool[source]

Tests whether both intervals finish at the same value.

intersect(other: Interval[T]) Interval[T] | None[source]

Intersect this interval with another one.

Returns

None if the intervals do not overlap else the intersection

join(other: Interval[T]) Interval[T] | None[source]

Join this interval with an interval that overlaps or meets this one.

Returns

None if the intervals do not overlap or meet else the union

property closure: Interval[T]

Return a closed variant of this interval

map(f: t.Callable[[T], U]) Interval[U][source]

Apply a monotonic function to the interval’s bounds.

For instance, the following would turn a datetime interval into a date interval by mapping dt↦dt.date():

>>> from datetime import datetime, date
>>> i = starting_from(datetime.fromisoformat("2020-01-01T00:00:00Z"))
>>> i_dates = i.map(lambda dt: dt.date())
>>> assert i_dates == starting_from(date(2020, 1, 1))

Note

This turns Interval into a functor from the category of linearly ordered types (i.e., SupportsAllComparisons) to the category of types. In other words, for every map \(f:T→U\), we get a corresponding map \(Ff: \mathrm{Interval}[T]→\mathrm{Interval}[U]\) in a way which

  • preserves identity, i.e. i.map(lambda x: x) == i

  • satisfies \(F(f\circ g) = Ff \circ Fg\), i.e. i.map(f).map(g) == i.map(lambda x: g(f(x))

Hint

monotonicity is necessary because otherwise there is some pair of values \((x, y)\) such that the values \((f(x), f(y))\) are invalid as bounds for an interval.

Parameters

f – A monotonic function (that is, a function satisfying \(x≤y⇒f(x)≤f(y)\) for all \(x, y: T\))

Returns

A new interval with f applied to its bounds

closed(begin: T | None, end: T | None) Interval[source]

Create a closed interval.

Returns

closed interval [begin, end]

closedopen(begin: T | None, end: T | None) Interval[source]

Create a left-closed/right-open interval.

Returns

left-closed/right-open interval [begin, end)

starting_from(when: T) Interval[source]

Alias for closedopen(when, None)

Returns

[when, ∞)

openclosed(begin: T | None, end: T | None) Interval[source]

Create a left-open/right-closed interval.

Returns

left-open/right-closed interval (begin, end]

open(begin: T | None, end: T | None) Interval[source]

Create an open interval.

Returns

open interval (begin, end)

single(point: T) Interval[source]

Create an interval containing only a single point.

empty(point: T) Interval[source]

Create an empty interval positioned at the given point.

It may seem a bit confusing that an empty interval has a location, but it eases the implementation of an interval a lot, as the empty interval does not need to be handled specially.

UnboundedInterval = ((pycroft.helpers.interval.NegativeInfinity, False), (pycroft.helpers.interval.PositiveInfinity, False))
class IntervalSet(intervals: pycroft.helpers.interval.IntervalSetSource = None)[source]
property length

Compute the total length of the interval set, i.e. the sum of all interval lengths.

The length is None if the set contains an unbounded interval.

Note: This is not the same as len(self), which is the number of non-overlapping intervals.

complement()[source]
union(other: IntervalSetSource) IntervalSet[T][source]
intersect(other: IntervalSetSource) IntervalSet[T][source]
difference(other: IntervalSetSource) IntervalSet[T][source]

pycroft.helpers.net

mac_regex = re.compile('\n    # MAC-Addresses are in big endian, hence we rightmost is the highest\n    # byte.\n\n    # Begin of string:\n    \\A\n    # First, most significant byte:\n    (?P<byte1>(?:[a-fA-F0-9]{2}))\n   , re.IGNORECASE|re.VERBOSE)

Regular expression object for matching MAC addresses in different formats. A valid MAC address is a sequence of 6 bytes coded as hexadecimal digits separated by a symbol after either one, two or three bytes. It is also possible that there is no separating symbol.

The following examples all encode the same MAC address:

  • 001122334455

  • 00-11-22-33-44-55

  • 00:11:22:33:44:55

  • 0011.2233.4455

  • 001122-334455

After a successful match, the individual bytes bytes, as well as the separator symbols can be accessed using the following symbolic group names (see groupdict()):

byte1, byte2, byte3, byte4, byte5, byte6

The n-th byte

sep1, sep2, sep3

The one, two or three byte separator char or None

ip_regex = re.compile('^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$')

regex matching (not necessarily valid) ip addresses

port_name_sort_key(port_name: str) int[source]
reverse_pointer(ip_address: IPAddress) str[source]
get_interface_manufacturer(mac: str) str | None[source]

pycroft.helpers.user

generate_password(length: int) str[source]

Generate a password of a certain length.

The password is generated as length independent choices of a certain charset. The charset does not include ambiguous characters like l, 1, 0 and O.

Param

length

hash_password(plaintext_passwd: str) str[source]

Generate a RFC 2307 complaint hash from given plaintext.

The passlib CryptContext is configured to generate the very secure ldap_sha512_crypt hashes (a crypt extension available since glibc 2.7).

cleartext_password(plaintext_passwd: str) str[source]

Generate a RFC 2307 complaint hash from given plaintext.

The passlib CryptContext is configured to generate the very secure ldap_sha512_crypt hashes (a crypt extension available since glibc 2.7).

verify_password(plaintext_password: str, hash: str) bool[source]

Verifies a plain password string against a given password hash.

It uses a crypt_context to verify RFC 2307 hashes.

login_hash(login: str) bytes[source]

Hashes a login with sha512, as is done in the User.login_hash generated column.

generate_random_str(length: int) str[source]

Generates an aplhanumeric random string

pycroft.helpers.utc

class TimeTz

A time with timezone information

alias of time

class DateTimeTz

A datetime with timezone information

alias of datetime

class DateTimeNoTz

A datetime without timezone information

alias of datetime

time_min() TimeTz[source]
time_max() TimeTz[source]
datetime_min() DateTimeTz[source]
datetime_max() DateTimeTz[source]
ensure_tz(d: datetime) DateTimeTz[source]
with_min_time(d: date) DateTimeTz[source]

Return the datetime corresponding to 00:00 UTC at the given date.

with_max_time(d: date) DateTimeTz[source]
safe_combine(d: date, t: TimeTz) DateTimeTz[source]
ensure_tzinfo(t: time) TimeTz[source]
combine_ensure_tzinfo(d: date, t: time) DateTimeTz[source]
combine_or_midnight(d: date, t: time | None) DateTimeTz[source]