chapps.util module

Utility classes

Since CHAPPS mainly deals with policy requests coming from Postfix, there is a utility object for representing them in a way which makes the code easier to read while also providing access optimizations.

There is another utility class for providing the configuration data via an object which presents dictionary keys as attributes.

In order to create different defaults and access documentation files, an object is provided which detects whether the library is running within a virtual environment, and serves as a source of local paths to package resources.

class chapps.util.VenvDetector[source]

Bases: object

Detect use of a virtual environment and calculate local paths

The detector encapsulates the job of determining whether a virtual environment is activated, and assists in composing some important paths in order to locate certain files the application needs.

One of those files is Markdown source which is imported into the live API documentation. Another is the config file.

Instance attributes:

datapath

Path pointing at location where data files are installed

ve

bool indicating whether a virtual environment is active

docpath

Path pointing at the location of the Markdown

confpath

Path pointing at the config file

venvpath

Path to the root of the active virtual environment, or None if none is active

__init__(*, datapath=None)[source]

Detect virtual environments and provide local package paths

Parameters

datapath (Union[str, Path, None]) – optional override to point at the data installation path of the package (or a surrogate). The API uses this value to load the API readme into the live docs.

get_base_prefix_compat()[source]

Return the non-virtual base prefix

Sometimes called sys.real_prefix, so we check for both.

Returns

the base path prefix

Return type

str

in_virtualenv()[source]

Compare prefixes to determine if a virtual environment is active.

Returns

true if a virtual environment is active, otherwise False

Return type

bool

sphinx_build()[source]

Determine whether invoked by Sphinx

Return type

bool

Returns

True if Sphinx invoked the library

property ve: bool

Property which memoizes in_virtualenv()

Return type

bool

property sb: bool

Property which memoizes sphinx_build()

Return type

bool

property docpath: pathlib.Path

Memoizes the documentation location

Returns

a Path pointing at the Markdown files’ location

Return type

pathlib.Path

property confpath: pathlib.Path

Memoizes the config file’s full path

Returns

a Path pointing at the config file

Return type

pathlib.Path

property venvpath: Optional[pathlib.Path]

The virtual environment root, if any

Returns

None or the value of sys.prefix as a Path

Return type

Optional[pathlib.Path]

If no virtual environment is active, then None is returned, otherwise a Path instance is returned, containing the path to the virtual environment. This hasn’t been tested with all types of virtual environment.

class chapps.util.AttrDict[source]

Bases: object

Attribute Dictionary

This simple class allows accessing the keys of a hash as attributes on an object. As a useful side effect it also casts floats, integers and booleans in advance.

This object is used in CHAPPSConfig for holding the configuration data.

Note

The purpose of this class is to map all the keys of a fairly small dict as attributes of the instance onto their values in the source dict. This class does not perform the same sort of lazy-loading as the PostfixPolicyRequest class below; it pre-maps all the elements in the source dict. So be careful about passing large dictionaries to it.

Subclassing

Given the stated purpose of the class, all internal instance attributes, i.e. ones not associated to a key-value pair in the source object, should begin with _ (an underscore).

boolean_pattern = re.compile('^[Tt]rue|[Ff]alse$')

A regex to detect text-string boolean values

__init__(data=None, **kwargs)[source]

Populate an instance with attributes

Parameters

If, and only if, data is not provided, then the keyword arguments will be used in place of data provided as a dict.

Henceforth whatever is rounded up to use shall be referred to as the data.

The initialization routine creates an attribute on the instance for each key in the data, and then attempts to cast the value:

  1. to an int.

  2. If a TypeError is encountered, the unadulterated value is used.

  3. If only ValueError is raised, then it is casted to float

  4. if that causes another ValueError then it is matched against the boolean_pattern to see whether it matches, which is to say, whether it is a string containing “true” or “false”.

  5. If so, a simple check is conducted to determine whether the match was four characters long: True.

  6. If it does not test positive for truth, it is considered to be False.

  7. But if it wasn’t a match for the boolean_pattern at all, then its original value is preserved.

Generally, instances of this class are used to present a particular module, such as a policy manager, with its configuration in a form which can be dereferenced with dot notation. As such, values which cannot be casted to some other type are almost always left in their original form as strings, because the AttrDict is being initialized with a sub-block of a ConfigParser as the source object, and its values will all be strings.

class chapps.util.PostfixPolicyRequest[source]

Bases: collections.abc.Mapping

Lazy-loading Policy Request Mapping Interface

An implementation of Mapping which by default only processes and caches values from the data payload when they are accessed, to avoid a bunch of useless parsing. Instances may be dereferenced like hashes, but the keys are also attributes on the instance, so they can be accessed without brackets and quotation marks.

Once parsed, results are memoized.

For example, a payload might look a bit like this, when it is first received from Postfix and turned into an array of one string per line:

payload = [
  "request=smtpd_access_policy",
  "protocol_state=RCPT",
  "protocol_name=SMTP",
  "helo_name=helo.chapps.io",
  "queue_id=8045F2AB23",
  "sender=unauth@easydns.com",
  "recipient=bar@foo.tld",
  "recipient_count=0",
  "client_address=10.10.10.10",
  "client_name=mail.chapps.io",
  "reverse_client_name=mail.chapps.io",
  "instance=a483.61706bf9.17663.0",
  "sasl_method=plain",
  "sasl_username=somebody@chapps.io",
  "sasl_sender=",
  "size=12345",
  "ccert_subject=",
  "ccert_issuer=Caleb+20Cullen",
  "ccert_fingerprint=DE:AD:BE:EF:FE:ED:AD:DE:D0:A7:52:F3:C1:DA:6E:04",
  "encryption_protocol=TLSv1/SSLv3",
  "encryption_cipher=DHE-RSA-AES256-SHA",
  "encryption_keysize=256",
  "etrn_domain=",
  "stress=",
  "ccert_pubkey_fingerprint=68:B3:29:DA:98:93:E3:40:99:C7:D8:AD:5C:B9:C9:40",
  "client_port=1234",
  "policy_context=submission",
  "server_address=10.3.2.1",
  "server_port=54321",
  "",
]

Refer to the Postfix policy delegation documentation for more information.

As an example of the class’s utility, and using the above definition of payload, consider:

from chapps.util import PostfixPolicyRequest

ppr = PostfixPolicyRequest(payload)

# all the following are true:
ppr.sender == 'unauth@easydns.com'
ppr.sasl_username == 'somebody@chapps.io'
ppr.client_address == '10.10.10.10'

# demonstrating the pseudo-attribute:
ppr.recipients == ['bar@foo.tld']

Instance attributes (apart from Postfix payload parameters):

_payload

the Postfix policy delegation request payload in string-per-line format

recipients

a pseudo-attribute of the policy request derived from the value of recipient, provided by Postfix, which may contain more than one comma-separated email address. For reasons unknown, Postfix always provides a recipient_count of 0 before the DATA phase, so we rely upon counting the email addresses directly.

_recipients

memoization attribute for recipients()

Subclassing

Because the purpose of the class is to present the contents of the initial payload as attributes, all internal attributes are prefaced with an underscore.

__getattr__(attr)[source]

Overloaded in order to search for missing attributes in the payload

Parameters

attr (str) – the attribute which triggered this call

Returns

the value found in the payload, or None

Return type

Optional[str]

First, if the value of attr starts with an underscore, None is returned. No lines of the payload start with an underscore. This ensures that references to internal attributes of the class are not snarled up with the payload searches.

Next, the payload is searched for the requested key-value pair, attempting to match attr against everything before the = sign. When a line is found, the contents after the = are stored as an attribute named attr (and so memoized), and the value is returned. Future attempts to obtain the value will encounter the attribute and not invoke __getattr__() again.

A DEBUG level message is currently produced if no lines in the payload matched the requested payload data. No errors are produced if a nonexistent attr starting with _ is encountered.

__init__(payload)[source]

Store the payload.

Parameters

payload (List[str]) – strings which are formatted as ‘key=val’, including an empty entry at the end.

This routine discards the last element of the list and stores the rest as self._payload.

property recipients: List[str]

Memoize recipients as a list

Returns

a list of strings which are the email addresses of recipients

Return type

List[str]

A convenience method to split the ‘recipient’ datum into comma-separated tokens for easier counting.