Package inkfill

Multi-document template engine.

Inky the Squid
Inky the Squid

Build PyPI Supported Python Versions

Why?

inkfill helps you generate multiple documents using jinja2 templates and toml configuration files.

License

MIT License

Expand source code
"""Multi-document template engine.

.. include:: ../../README.md
   :start-line: 2
"""
from .numerals import commafy
from .numerals import NumFormat
from .numerals import to_alpha
from .numerals import to_cardinal
from .numerals import to_decimal
from .numerals import to_nth
from .numerals import to_ordinal
from .numerals import to_roman

from .filters import compound
from .filters import day_of_month
from .filters import dollars
from .filters import USD

from .xref import Division
from .xref import Ref
from .xref import RefFormat
from .xref import Refs
from .xref import slugify

__version__ = "0.1.0"
__pubdate__ = ""

__all__ = [
    "__version__",
    "__pubdate__",
    # numeric
    "commafy",
    "NumFormat",
    "to_alpha",
    "to_cardinal",
    "to_decimal",
    "to_nth",
    "to_ordinal",
    "to_roman",
    # filters
    "compound",
    "day_of_month",
    "dollars",
    "USD",
    # xref
    "Division",
    "Ref",
    "RefFormat",
    "Refs",
    "slugify",
]

Sub-modules

inkfill.filters

Common helper functions.

inkfill.numerals
inkfill.registry

Name registry.

inkfill.xref

Cross-reference helpers.

Functions

def commafy(num: int) ‑> str

Return a comma-grouped number.

Expand source code
def commafy(num: int) -> str:
    """Return a comma-grouped number."""
    return f"{num:,}"
def to_alpha(num: int) ‑> str

English alphabetical numerals.

Expand source code
def to_alpha(num: int) -> str:
    """English alphabetical numerals."""
    # https://en.wikipedia.org/wiki/English_alphabet
    result = ""
    if num < 1:
        return result

    while num > 0:
        num, remainder = divmod(num - 1, 26)
        result = ALPHABET[remainder] + result

    return result
def to_cardinal(num: int) ‑> str

English cardinal numerals.

Expand source code
def to_cardinal(num: int) -> str:
    """English cardinal numerals."""
    # https://en.wikipedia.org/wiki/Cardinal_numeral
    # See: https://stackoverflow.com/a/54018199
    if num == 0:
        return "zero"
    if num < 0:
        return _space_join("negative", _pos(-num))
    return _pos(num)
def to_decimal(num: int) ‑> str

Decimal numerals.

Expand source code
def to_decimal(num: int) -> str:
    """Decimal numerals."""
    # https://en.wikipedia.org/wiki/Decimal
    return str(num)
def to_nth(num: int) ‑> str

Numeric ordinal numerals ("1st", "2nd").

Expand source code
def to_nth(num: int) -> str:
    """Numeric ordinal numerals ("1st", "2nd")."""
    # https://en.wikipedia.org/wiki/Ordinal_numeral
    if 10 <= num % 100 <= 20:
        suffix = "th"
    else:
        suffixes = {1: "st", 2: "nd", 3: "rd"}
        suffix = suffixes.get(num % 10, "th")
    return str(num) + suffix
def to_ordinal(num: int) ‑> str

English ordinal numerals ("first", "second").

Expand source code
def to_ordinal(num: int) -> str:
    """English ordinal numerals ("first", "second")."""
    # https://en.wikipedia.org/wiki/Ordinal_numeral
    cardinal = to_cardinal(num)
    for check, suffix in ORDINAL_SUFFIXES.items():
        if cardinal.endswith(check):
            return cardinal[: -len(check)] + suffix
    return cardinal + "th"
def to_roman(num: int) ‑> str

Return the Roman numeral.

Expand source code
def to_roman(num: int) -> str:
    """Return the [Roman numeral](https://en.wikipedia.org/wiki/Roman_numerals)."""
    result = ""
    if num < 1:
        return result

    idx = 0
    while num > 0:
        for _ in range(num // ROMAN_VALS[idx]):
            result += ROMAN_SYMS[idx]
            num -= ROMAN_VALS[idx]
        idx += 1
    return result
def compound(clauses: List[str], conjunction: Literal['for', 'and', 'nor', 'but', 'or', 'yet', 'so'] = 'and', oxford: bool = True) ‑> str

Return clauses connected with conjunction and Oxford comma (optional).

Expand source code
def compound(
    clauses: List[str],
    conjunction: FANBOYS = "and",
    oxford: bool = True,
) -> str:
    """Return `clauses` connected with `conjunction` and Oxford comma (optional)."""
    L = len(clauses)
    if L == 0:
        return ""
    if L == 1:
        return clauses[0]
    if L == 2:
        return f" {conjunction} ".join(clauses)

    first = ", ".join(clauses[0:-1])
    last = clauses[-1]
    return f"{first}{',' if oxford else ''} {conjunction} {last}"
def day_of_month(dt: datetime.datetime) ‑> str

Return date as {nth} day of {month} {year}.

Expand source code
def day_of_month(dt: datetime) -> str:
    """Return date as `{nth} day of {month} {year}`."""
    return f"{to_nth(dt.day)} day of {dt.strftime('%B %Y')}"
def dollars(num: float) ‑> str

Return spelled-out dollars. Commonly used in contracts.

Expand source code
def dollars(num: float) -> str:
    """Return spelled-out dollars. Commonly used in contracts."""
    dec = Decimal(str(num))

    whole = int(dec)
    part = dec % 1
    if Decimal("0") < part < Decimal("0.01"):  # sub-cent
        return USD(num, cents=True)

    result = ""
    if whole or num == 0:
        result += f"{say_number(whole)} dollar{'s' if whole != 1 else ''}"

    if whole and part:
        result += " and "

    if part:
        part *= 100
        result += f"{say_number(int(part))} cent{'s' if part != 1 else ''}"
    elif num != 0:
        result += " exactly"

    result += f" ({USD(num, cents=part > 0 or num < 10)})"
    return result
def USD(num: float, cents: bool = False) ‑> str

Return formatted US Dollars.

Expand source code
def USD(num: float, cents: bool = False) -> str:
    """Return formatted US Dollars."""
    if cents:
        if 0 < num < 0.01:
            return f"US${num}"
        return f"US${num:,.2f}"
    return f"US${num:,}"
def slugify(*names: str) ‑> str

Return a slug version of a name.

Expand source code
def slugify(*names: str) -> str:
    """Return a slug version of a name."""
    return RE_REPEATED_DASH.sub(
        "-", RE_NON_SLUG.sub("-", "-".join(names).lower())
    ).strip("-")

Classes

class NumFormat (name: str, numeral: NUMERAL_FUNC, prefix: str = '', suffix: str = '')

Numeral formatter.

Expand source code
class NumFormat(Registrable):
    """Numeral formatter."""

    name: str
    """Numeral system name."""

    numeral: NUMERAL_FUNC
    """Function that converts an integer to a numeral."""

    prefix: str = ""
    """Prefix string when rendering."""

    suffix: str = ""
    """Suffix string when rendering."""

    def render(self, num: int, punctuation: bool = False) -> str:
        """Render the numeral."""
        val = self.numeral(num)
        return val if not punctuation else f"{self.prefix}{val}{self.suffix}"

    __call__ = render

Ancestors

Class variables

var name : str

Inherited from: Registrable.name

Name of this object.

var numeral : Callable[[int], str]

Function that converts an integer to a numeral.

var prefix : str

Prefix string when rendering.

var suffix : str

Suffix string when rendering.

var DEFAULTRegistrable

Inherited from: Registrable.DEFAULT

Default value of this type.

Static methods

def get(name: Union[str, T], default: Optional[T] = None) ‑> ~T

Inherited from: Registrable.get

Get the requested named object.

Methods

def render(self, num: int, punctuation: bool = False) ‑> str

Render the numeral.

Expand source code
def render(self, num: int, punctuation: bool = False) -> str:
    """Render the numeral."""
    val = self.numeral(num)
    return val if not punctuation else f"{self.prefix}{val}{self.suffix}"
def add(self: T, override: bool = False) ‑> ~T

Inherited from: Registrable.add

Add this name to the registry.

class Division (name: str, numeral: NumFormat = NumFormat(name='decimal', numeral=<function to_decimal>, prefix='.', suffix=''), define: RefFormat = RefFormat(name='cite-name', render=<function cite_name>), refer: RefFormat = RefFormat(name='kind-cite', render=<function <lambda>>))

Document section.

Expand source code
class Division(Registrable):
    """Document section."""

    name: str
    """Name of this kind."""

    numeral: NumFormat = field(default=DECIMAL)
    """Default numeral formatting for this kind."""

    define: RefFormat = field(default=CITE_NAME)
    """Default formatting for defining a reference of this type."""

    refer: RefFormat = field(default=CITE_FULL)
    """Default formatting for referring to a reference of this type."""

    def __str__(self) -> str:
        return self.name

Ancestors

Class variables

var name : str

Inherited from: Registrable.name

Name of this object.

var numeralNumFormat

Default numeral formatting for this kind.

var defineRefFormat

Default formatting for defining a reference of this type.

var referRefFormat

Default formatting for referring to a reference of this type.

var DEFAULTRegistrable

Inherited from: Registrable.DEFAULT

Default value of this type.

Static methods

def get(name: Union[str, T], default: Optional[T] = None) ‑> ~T

Inherited from: Registrable.get

Get the requested named object.

Methods

def add(self: T, override: bool = False) ‑> ~T

Inherited from: Registrable.add

Add this name to the registry.

class Ref (name: str = '', slug: str = '', kind: Division = Division(name='Section', numeral=NumFormat(name='decimal', numeral=<function to_decimal>, prefix='.', suffix=''), define=RefFormat(name='cite-name', render=<function cite_name>), refer=RefFormat(name='kind-cite', render=<function <lambda>>)), values: List[int] = <factory>, numerals: List[NumFormat] = <factory>, defines: List[RefFormat] = <factory>, refers: List[RefFormat] = <factory>, is_defined: bool = False)

Reference to a term or section of a document.

Expand source code
class Ref:
    """Reference to a term or section of a document."""

    # `name` and `slug` are for referencing
    # `kind` and formats are for levels/sectioning.

    name: str = ""
    """Term or section title."""

    slug: str = ""
    """Unique ID of this reference (should include `kind` + `name`)."""

    kind: Division = field(default=Section)
    """What kind of reference is this?"""

    values: List[int] = field(default_factory=list)
    """Citation numeral values."""

    numerals: List[NumFormat] = field(default_factory=list)
    """Citation numeral formats."""

    defines: List[RefFormat] = field(default_factory=list)
    """Definition formats."""

    refers: List[RefFormat] = field(default_factory=list)
    """Reference formats."""

    is_defined: bool = False
    """Whether or not this reference has been defined."""

    def copy(self) -> Ref:
        """Return a copy of this reference."""
        return Ref(
            name=self.name,
            slug=self.slug,
            kind=self.kind,
            values=self.values.copy(),
            numerals=self.numerals.copy(),
            defines=self.defines.copy(),
            refers=self.refers.copy(),
        )

    def update_slug(self, slug: str = "") -> Ref:
        """Update the slug when the name or citation has changed."""
        if slug:
            self.slug = slugify(slug)
        elif self.name:
            self.slug = slugify(f"{self.kind}-{self.name}")
        else:
            self.slug = slugify(f"{self.kind}-{self.cite}")
        return self

    @property
    def cite(self) -> str:
        """Return a citation (without the `kind`) to this reference."""
        return "".join(
            num(val, idx > 0)
            for idx, (val, num) in enumerate(zip(self.values, self.numerals))
        )

    @property
    def above_below(self) -> str:
        """Return 'above' if defined, otherwise 'below'."""
        return "above" if self.is_defined else "below"

    def define(self) -> str:
        """Return the reference definition."""
        if self.is_defined:
            raise Exception(f"{self.kind} {self.slug} already defined")
        self.is_defined = True

        refer = self.kind.refer if len(self.refers) == 0 else self.refers[-1]
        define = self.kind.define if len(self.defines) == 0 else self.defines[-1]
        return f"""<span class="def" id="{self.slug}" data-kind="{self.kind}"
                    data-cite="{refer(self).strip()}">{define(self).strip()}</span>"""

    def refer(self) -> str:
        """Return a link to the reference definition."""
        refer = self.kind.refer if len(self.refers) == 0 else self.refers[-1]
        return f"""<a class="ref" href="#{self.slug}"
                    data-kind="{self.kind}">{refer(self).strip()}</a>"""
        # return f"""<a class="ref" href="#{self.slug}" data-kind="{self.kind}"></a>"""

    __str__ = refer

Class variables

var values : List[int]

Citation numeral values.

var numerals : List[NumFormat]

Citation numeral formats.

var defines : List[RefFormat]

Definition formats.

var refers : List[RefFormat]

Reference formats.

var name : str

Term or section title.

var slug : str

Unique ID of this reference (should include kind + name).

var kindDivision

What kind of reference is this?

var is_defined : bool

Whether or not this reference has been defined.

Instance variables

var cite : str

Return a citation (without the kind) to this reference.

Expand source code
@property
def cite(self) -> str:
    """Return a citation (without the `kind`) to this reference."""
    return "".join(
        num(val, idx > 0)
        for idx, (val, num) in enumerate(zip(self.values, self.numerals))
    )
var above_below : str

Return 'above' if defined, otherwise 'below'.

Expand source code
@property
def above_below(self) -> str:
    """Return 'above' if defined, otherwise 'below'."""
    return "above" if self.is_defined else "below"

Methods

def copy(self) ‑> Ref

Return a copy of this reference.

Expand source code
def copy(self) -> Ref:
    """Return a copy of this reference."""
    return Ref(
        name=self.name,
        slug=self.slug,
        kind=self.kind,
        values=self.values.copy(),
        numerals=self.numerals.copy(),
        defines=self.defines.copy(),
        refers=self.refers.copy(),
    )
def update_slug(self, slug: str = '') ‑> Ref

Update the slug when the name or citation has changed.

Expand source code
def update_slug(self, slug: str = "") -> Ref:
    """Update the slug when the name or citation has changed."""
    if slug:
        self.slug = slugify(slug)
    elif self.name:
        self.slug = slugify(f"{self.kind}-{self.name}")
    else:
        self.slug = slugify(f"{self.kind}-{self.cite}")
    return self
def define(self) ‑> str

Return the reference definition.

Expand source code
def define(self) -> str:
    """Return the reference definition."""
    if self.is_defined:
        raise Exception(f"{self.kind} {self.slug} already defined")
    self.is_defined = True

    refer = self.kind.refer if len(self.refers) == 0 else self.refers[-1]
    define = self.kind.define if len(self.defines) == 0 else self.defines[-1]
    return f"""<span class="def" id="{self.slug}" data-kind="{self.kind}"
                data-cite="{refer(self).strip()}">{define(self).strip()}</span>"""
def refer(self) ‑> str

Return a link to the reference definition.

Expand source code
def refer(self) -> str:
    """Return a link to the reference definition."""
    refer = self.kind.refer if len(self.refers) == 0 else self.refers[-1]
    return f"""<a class="ref" href="#{self.slug}"
                data-kind="{self.kind}">{refer(self).strip()}</a>"""
    # return f"""<a class="ref" href="#{self.slug}" data-kind="{self.kind}"></a>"""
class RefFormat (name: str, render: Callable[[Ref], str])

Reference format.

Expand source code
class RefFormat(Registrable):
    """Reference format."""

    name: str
    """Reference format name."""

    render: Callable[[Ref], str]
    """Render the reference."""

    def __call__(self, ref: Ref) -> str:
        """Render the reference."""
        return self.render(ref)

Ancestors

Class variables

var name : str

Inherited from: Registrable.name

Name of this object.

var render : Callable[[Ref], str]

Render the reference.

var DEFAULTRegistrable

Inherited from: Registrable.DEFAULT

Default value of this type.

Static methods

def get(name: Union[str, T], default: Optional[T] = None) ‑> ~T

Inherited from: Registrable.get

Get the requested named object.

Methods

def add(self: T, override: bool = False) ‑> ~T

Inherited from: Registrable.add

Add this name to the registry.

class Refs

Reference manager.

Construct a new reference manager.

Expand source code
class Refs:
    """Reference manager."""

    stack: List[Ref]
    """Current document levels."""

    store: Dict[str, Ref]
    """Slugs mapped to references."""

    def __init__(self) -> None:
        """Construct a new reference manager."""
        self.reset()

    def reset(self) -> Refs:
        """Reset the references."""
        self.stack = []
        self.store = {}
        return self

    @property
    def undefined(self) -> List[Ref]:
        """References that were never defined."""
        return [ref for ref in self.store.values() if not ref.is_defined]

    @property
    def current(self) -> Ref:
        """Reference to current level."""
        return Ref() if len(self.stack) == 0 else self.stack[-1]

    def push(
        self,
        kind: Union[str, Division] = Section,
        # formatting overrides
        numeral: Union[str, NumFormat, None] = None,
        define: Union[str, RefFormat, None] = None,
        refer: Union[str, RefFormat, None] = None,
    ) -> Refs:
        """Add another level."""
        level = self.current.copy()
        level.kind = Division.get(kind) or Section
        level.values.append(0)
        level.numerals.append(NumFormat.get(numeral, level.kind.numeral))
        level.defines.append(RefFormat.get(define, level.kind.define))
        level.refers.append(RefFormat.get(refer, level.kind.refer))
        self.stack.append(level)
        return self

    def up(self, name: str = "", slug: str = "") -> Ref:
        """Increment current level."""
        ref = self.current.copy()
        ref.name = name
        ref.values[-1] += 1
        ref.update_slug(slug)  # needs name & values updated
        self.stack[-1] = ref
        self.store[ref.slug] = ref
        return ref

    def pop(self, num: int = 1) -> Refs:
        """Remove one or more level."""
        for _ in range(num):
            if len(self.stack) == 0:
                break
            self.stack.pop()
        return self

    ## Short-hand
    def add(self, ref: Ref) -> Ref:
        """Add a ref to the store."""
        self.store[ref.slug] = ref
        return ref

    def term(self, name: str) -> Ref:
        """Refer to a terms."""
        slug = slugify("term", name)
        if slug in self.store:
            return self.store[slug]
        return self.add(Ref(name=name, kind=Term).update_slug())

    def see(self, name: str = "", kind: str = "Section", slug: str = "") -> Ref:
        """Refer to a reference."""
        if slug in self.store:
            return self.store[slug]
        return self.add(Ref(name=name, kind=Division.get(kind)).update_slug(slug))

Class variables

var stack : List[Ref]

Current document levels.

var store : Dict[str, Ref]

Slugs mapped to references.

Instance variables

var undefined : List[Ref]

References that were never defined.

Expand source code
@property
def undefined(self) -> List[Ref]:
    """References that were never defined."""
    return [ref for ref in self.store.values() if not ref.is_defined]
var currentRef

Reference to current level.

Expand source code
@property
def current(self) -> Ref:
    """Reference to current level."""
    return Ref() if len(self.stack) == 0 else self.stack[-1]

Methods

def reset(self) ‑> Refs

Reset the references.

Expand source code
def reset(self) -> Refs:
    """Reset the references."""
    self.stack = []
    self.store = {}
    return self
def push(self, kind: Union[str, Division] = Division(name='Section', numeral=NumFormat(name='decimal', numeral=<function to_decimal>, prefix='.', suffix=''), define=RefFormat(name='cite-name', render=<function cite_name>), refer=RefFormat(name='kind-cite', render=<function <lambda>>)), numeral: Union[str, NumFormat, None] = None, define: Union[str, RefFormat, None] = None, refer: Union[str, RefFormat, None] = None) ‑> Refs

Add another level.

Expand source code
def push(
    self,
    kind: Union[str, Division] = Section,
    # formatting overrides
    numeral: Union[str, NumFormat, None] = None,
    define: Union[str, RefFormat, None] = None,
    refer: Union[str, RefFormat, None] = None,
) -> Refs:
    """Add another level."""
    level = self.current.copy()
    level.kind = Division.get(kind) or Section
    level.values.append(0)
    level.numerals.append(NumFormat.get(numeral, level.kind.numeral))
    level.defines.append(RefFormat.get(define, level.kind.define))
    level.refers.append(RefFormat.get(refer, level.kind.refer))
    self.stack.append(level)
    return self
def up(self, name: str = '', slug: str = '') ‑> Ref

Increment current level.

Expand source code
def up(self, name: str = "", slug: str = "") -> Ref:
    """Increment current level."""
    ref = self.current.copy()
    ref.name = name
    ref.values[-1] += 1
    ref.update_slug(slug)  # needs name & values updated
    self.stack[-1] = ref
    self.store[ref.slug] = ref
    return ref
def pop(self, num: int = 1) ‑> Refs

Remove one or more level.

Expand source code
def pop(self, num: int = 1) -> Refs:
    """Remove one or more level."""
    for _ in range(num):
        if len(self.stack) == 0:
            break
        self.stack.pop()
    return self
def add(self, ref: Ref) ‑> Ref

Add a ref to the store.

Expand source code
def add(self, ref: Ref) -> Ref:
    """Add a ref to the store."""
    self.store[ref.slug] = ref
    return ref
def term(self, name: str) ‑> Ref

Refer to a terms.

Expand source code
def term(self, name: str) -> Ref:
    """Refer to a terms."""
    slug = slugify("term", name)
    if slug in self.store:
        return self.store[slug]
    return self.add(Ref(name=name, kind=Term).update_slug())
def see(self, name: str = '', kind: str = 'Section', slug: str = '') ‑> Ref

Refer to a reference.

Expand source code
def see(self, name: str = "", kind: str = "Section", slug: str = "") -> Ref:
    """Refer to a reference."""
    if slug in self.store:
        return self.store[slug]
    return self.add(Ref(name=name, kind=Division.get(kind)).update_slug(slug))