Skip to content

Reference🔗

berhoel.ctitools 🔗

Work with cti index files for the Heise papers c’t and iX.

Modules:

  • __main__ –

    Main code for module.

  • ct –

    Manage entries for c’t.

  • cti2bibtex –

    Export cti as BiBTeX for Zotero.

  • ctientry –

    Base class for cti (c’t iX) entries.

  • ix –

    Manage entries for iX.

  • tests –

    Tests for ctitools.

Classes:

  • CTI –

    Read entries from cti files.

  • IssueData –

    Data for preparing issue instances.

  • PaperInfo –

    Information on current paper.

  • Papers –

    Distinguish papers.

Functions:

Classes🔗

berhoel.ctitools.CTI 🔗

CTI(
    infile: Path | str,
    limit_year: int | None = None,
    limit_issue: int | None = None,
    limit_journal: str | None = None,
)

Read entries from cti files.

  Bürokratie: Mit analoger Wucht

  Tim Gerber
  tig
    3
  16
  c22

  Standpunkt,Immer in c't,Gesellschaft,Ukraine-Krieg,Ukraine-Hilfe,Digitalisierung

Parameters:

  • infile 🔗
    (Path | str) –

    Input file

  • limit_year 🔗
    (int | None, default: None ) –

    Limit output to given year

  • limit_issue 🔗
    (int | None, default: None ) –

    Limit output to given issue

  • limit_journal 🔗
    (str | None, default: None ) –

    Limit output to given journal

Methods:

Source code in src/berhoel/ctitools/__init__.py
def __init__(
    self,
    infile: Path | str,
    limit_year: int | None = None,
    limit_issue: int | None = None,
    limit_journal: str | None = None,
) -> None:
    """Read input file.

    Args:
        infile: Input file
        limit_year: Limit output to given year
        limit_issue: Limit output to given issue
        limit_journal: Limit output to given journal
    """
    self.__entries = []
    self.limit_year = limit_year
    self.limit_issue = limit_issue
    self.limit_journal = limit_journal
    if zipfile.is_zipfile(infile):
        with zipfile.ZipFile(infile) as thiszip:
            infolist = thiszip.infolist()
            for info in infolist:
                extension = info.filename.split(".")[-1]
                if extension in {"frm", "cti"}:
                    with thiszip.open(info, "r") as inp:
                        self.__entries.extend(asyncio.run(self._gen_data(inp)))
    else:
        if isinstance(infile, str):
            infile = Path(infile)
        with infile.open("rb") as inp:
            self.__entries.extend(asyncio.run(self._gen_data(inp)))
Functions🔗
__iter__ 🔗
__iter__() -> Iterator[CTIEntry]

Prepare interator.

Source code in src/berhoel/ctitools/__init__.py
def __iter__(self) -> Iterator[CTIEntry]:
    """Prepare interator."""
    return iter(self.__entries)
fix_author staticmethod 🔗
fix_author(author: str) -> tuple[str, ...]

Fix author information.

Parameters:

  • author 🔗 (str) –

    list of authors

Returns:

Source code in src/berhoel/ctitools/__init__.py
@staticmethod
def fix_author(author: str) -> tuple[str, ...]:
    """Fix author information.

    Args:
        author: list of authors

    Returns:
        list of autors
    """
    if author.count(",") > 0 and author.count(",") == author.count(" "):
        res = [
            " ".join(j.strip() for j in i.split(",")[::-1])
            for i in author.split("/")
        ]
        author = ",".join(res)
    author = author.replace(" und ", ", ")
    author = author.replace("Von ", "")
    author = "Dušan".join(CTI.dusan_replace_re.split(author))
    author = "Živadinović".join(CTI.zivadinovic_replace_re.split(author))
    author = author.replace('M"cker', "Möcker")

    return tuple([i.strip() for i in author.split(",")])
fix_chars staticmethod 🔗
fix_chars(inp: bytes) -> bytes

Fix characters in input string.

Parameters:

Returns:

  • bytes –

    string with characters fixed

Source code in src/berhoel/ctitools/__init__.py
@staticmethod
def fix_chars(inp: bytes) -> bytes:
    """Fix characters in input string.

    Args:
        inp: input string

    Returns:
        string with characters fixed
    """
    table = bytes.maketrans(
        b"\334\344\374\366\337\351",
        b"\232\204\201\224\341\202",
    )
    return inp.translate(table).replace(b"\307\317", b"\204")

berhoel.ctitools.IssueData 🔗

Data for preparing issue instances.

berhoel.ctitools.PaperInfo dataclass 🔗

PaperInfo(paper: Papers = NONE, year: int = -1)

Information on current paper.

Methods:

Functions🔗
from_match staticmethod 🔗
from_match(match: Match | None) -> PaperInfo

Get PaperInfo from match object.

Source code in src/berhoel/ctitools/__init__.py
@staticmethod
def from_match(match: re.Match | None) -> PaperInfo:
    """Get PaperInfo from match object."""
    if False:
        _info = {"paper": Papers.NONE, "year": "-1"}
        if match is not None:
            groups = match.groupdict()
            year = groups["year"]
            if not isinstance(year, str):
                raise TypeError
            _info = {
                "paper": Papers.ct if groups["paper"] == "i" else Papers.iX,
                "year": int(year),
            }
    if match is None:
        return PaperInfo()

    return PaperInfo(
        Papers.from_char(match.groupdict()["paper"]), int(match.groupdict()["year"])
    )

berhoel.ctitools.Papers 🔗

Distinguish papers.

Methods:

  • from_char –

    Create Papers instance from character.

Functions🔗
from_char staticmethod 🔗
from_char(c: str) -> Papers

Create Papers instance from character.

Source code in src/berhoel/ctitools/__init__.py
@staticmethod
def from_char(c: str) -> Papers:
    """Create Papers instance from character."""
    return {"c": Papers.ct, "i": Papers.iX}.get(c, Papers.NONE)

Functions🔗

berhoel.ctitools.cti_statistics 🔗

cti_statistics(cti: Path) -> None

Print statistics to CTI File.

List number of articles for each issue found in input file.

CTI: input file, cti, frm, or zip file containing one of the previous (required)

Source code in src/berhoel/ctitools/__init__.py
@click.command()
@click.argument("cti", type=Path)
@click.version_option(version=f"{metadata.version('ctitools')}")
def cti_statistics(cti: Path) -> None:
    """Print statistics to CTI File.

    List number of articles for each issue found in input file.

    `CTI`: input file, cti, frm, or zip file containing one of the  previous
         (required)
    """
    cti_data = CTI(cti)

    data: defaultdict = defaultdict(lambda: defaultdict(lambda: defaultdict(int)))

    console = Console()

    for entry in cti_data:
        paper = entry.info.paper
        year, issue = entry.issue.split("/")  # type: str, str | int
        with suppress(ValueError):
            if isinstance(issue, str):
                issue = issue.strip()
                issue = int(issue)
        data[paper][int(year)][issue] += 1

    for paper in (Papers.iX, Papers.ct):
        table = Table(title=f"{paper}")
        years = sorted(data[paper].keys())
        for year in years:
            table.add_row(f"{year}")
            issues = sorted(data[paper][year].keys(), key=issue_key)
            s_issues = [f"{i}" for i in issues]
            s_issues = [f"{i:>{max(len(i), 3)}}" for i in s_issues]
            table.add_row(*s_issues)
            table.add_row(
                *(
                    f"{data[paper][year][i]:>{len(s)}}"
                    for i, s in zip(issues, s_issues, strict=False)
                ),
            )
        console.print(table)

berhoel.ctitools.issue_key 🔗

issue_key(key: str | int) -> int

Return sort key for c’t issues.

Parameters:

Source code in src/berhoel/ctitools/__init__.py
def issue_key(key: str | int) -> int:
    """Return sort key for c't issues.

    Args:
        key: issue key
    """
    month_sort = {
        "Januar": 1,
        "Februar": 2,
        "März": 3,
        "April": 4,
        "Mai": 5,
        "Juni": 6,
        "Juli": 7,
        "August": 8,
        "September": 9,
        "Oktober": 10,
        "November": 11,
        "Dezember": 12,
        "retro": 27,
        "ausblick": 27,
        "c't Jahresrückblick": 27,
    }
    if isinstance(key, str):
        if match := re.match(r"(?P<num>\d{1,2})", key):
            return int(match.group("num"))
        return month_sort[key]
    return key

berhoel.ctitools.ct 🔗

Manage entries for c’t.

Classes:

  • Ct –

    Prepare c’t issue information.

  • IssueMap –

    Class for determinig issue date for c’t issues.

Classes🔗

berhoel.ctitools.ct.Ct 🔗

Prepare c’t issue information.

Parameters:

Methods:

Source code in src/berhoel/ctitools/ct.py
def __init__(
    self,
    data: IssueData,
) -> None:
    """Add information for a c't issue.

    Args:
        data: data describing c't issue
    """
    full_issue = self.year_issue2full_issue(data.year, data.issue)
    self.date = self.issue_map(data.year, data.issue)
    self.shorttitle: str | None
    self.title: str
    if data.title is None:
        self.shorttitle, self.title = None, data.shorttitle
    else:
        self.shorttitle, self.title = (
            data.shorttitle,
            data.title,
        )
    self.author = data.author
    self.pages = data.pages
    self.full_issue = full_issue
    self.info = data.info
    self.references = data.references
    self.keywords = data.keywords
Functions🔗
__call__ 🔗
__call__() -> CTIEntry

Return CTIEntry for content.

Returns:

Source code in src/berhoel/ctitools/ct.py
def __call__(self) -> CTIEntry:
    """Return `CTIEntry` for content.

    Returns:
        `CTIEntry` instance
    """
    return CTIEntry(
        self.shorttitle,
        self.title,
        self.author,
        self.pages,
        self.full_issue,
        self.info,
        "c't magazin für computertechnik",
        self.date.strftime("%Y-%m-%d"),
        self.references,
        self.keywords,
    )
year_issue2full_issue staticmethod 🔗
year_issue2full_issue(year: int, issue: int) -> str

Retrieve full issue for c’t from year and issue number.

Parameters:

  • year 🔗 (int) –

    issue year

  • issue 🔗 (int) –

    issue number in year

Returns:

  • str –

    correctly formatted issue number string

Source code in src/berhoel/ctitools/ct.py
@staticmethod
def year_issue2full_issue(year: int, issue: int) -> str:
    """Retrieve full issue for c't from year and issue number.

    Args:
        year: issue year
        issue: issue number in year

    Returns:
        correctly formatted issue number string
    """
    tmp_issue: int | str = issue
    if tmp_issue == IssueMap.SPECIAL_ISSUE:
        if year in {2022}:
            tmp_issue = "c't Jahresrückblick"
        elif year in {2023, 2024}:
            pass
        elif year >= IssueMap.YEAR_FOR_RETRO_STARTING_AS_SPECIAL_ISSUE:
            tmp_issue = "retro"
    if year < IssueMap.YEAR_FOR_MONTLY_TO_2WEEK_FREQUENCY or (
        year == IssueMap.YEAR_FOR_MONTLY_TO_2WEEK_FREQUENCY
        and issue < IssueMap.LAST_MONTHLY_ISSUE_IN_1997
    ):
        return f"{year:04d} / {Ct.month_issue_map[issue]}"
    if year == 2024 and issue == 25:  # noqa: PLR2004
        return f"{year:04d} / {issue}: Das c't-Bastel-Kompendium"
    return f"{year:04d} / {tmp_issue}"

berhoel.ctitools.ct.IssueMap 🔗

IssueMap()

Class for determinig issue date for c’t issues.

Methods:

  • __call__ –

    Generate release dates for c’t.

Source code in src/berhoel/ctitools/ct.py
def __init__(self) -> None:
    """Intitalize."""
    self._issue_max: tuple[int, int] = (2022, 16)
    self._issue_min: tuple[int, int] = self._issue_max
    self._date_max: datetime = IssueMap.DATETIME(year=2022, month=7, day=16)
    self._date_min: datetime = self._date_max
    self._cache: dict[tuple[int, int], datetime] = {
        self._issue_max: self._date_max,
        (2018, 27): IssueMap.DATETIME(2018, 10, 23),
        (2019, 27): IssueMap.DATETIME(2019, 10, 21),
        (2020, 27): IssueMap.DATETIME(2020, 10, 20),
        (2021, 27): IssueMap.DATETIME(2021, 10, 19),
        (2022, 27): IssueMap.DATETIME(2022, 11, 26),
        (2023, 1): IssueMap.DATETIME(2022, 12, 17),
        (2023, 11): IssueMap.DATETIME(2023, 5, 6),
        (2023, 12): IssueMap.DATETIME(2023, 5, 13),
        (2023, 13): IssueMap.DATETIME(2023, 5, 20),
        (2023, 26): IssueMap.DATETIME(2023, 11, 11),
        (2023, 27): IssueMap.DATETIME(2023, 11, 18),
        (2023, 28): IssueMap.DATETIME(2023, 12, 2),
        (2023, 29): IssueMap.DATETIME(2023, 12, 16),
        (2024, 2): IssueMap.DATETIME(2024, 1, 12),
        (2024, 11): IssueMap.DATETIME(2024, 5, 10),
        (2024, 12): IssueMap.DATETIME(2024, 5, 17),
        (2024, 25): IssueMap.DATETIME(2024, 11, 6),
        (2024, 26): IssueMap.DATETIME(2024, 11, 15),
        (2024, 27): IssueMap.DATETIME(2024, 11, 29),
        (2024, 28): IssueMap.DATETIME(2024, 12, 13),
        (2025, 21): IssueMap.DATETIME(2025, 10, 2),
        (2025, 22): IssueMap.DATETIME(2025, 10, 17),
    }
Functions🔗
__add_issues_up 🔗
__add_issues_up(diff: timedelta, key: tuple[int, int]) -> tuple[int, int]

Add issues later then last known issue.

Parameters:

Raises:

Source code in src/berhoel/ctitools/ct.py
def __add_issues_up(self, diff: timedelta, key: tuple[int, int]) -> tuple[int, int]:
    """Add issues later then last known issue.

    Args:
        diff: difference between issues
        key: requested issue

    Raises:
        AssertionError: if inconsisten data detected
    """
    step_year, step_issue = self._issue_max
    step_issue += 1
    self._issue_max = (step_year, step_issue)
    if self._issue_max not in self._cache:
        if step_issue >= IssueMap.SPECIAL_ISSUE:
            step_issue = 1
            step_year += 1
            self._issue_max = (step_year, step_issue)
        self._date_max += diff
        self._cache[self._issue_max] = self._date_max
    elif self._date_max < self._cache[self._issue_max]:
        self._date_max = self._cache[self._issue_max]
    if self._date_max >= datetime.now(tz=UTC) + timedelta(
        days=16,
    ):
        msg = (
            f"{self._date_max=} < "
            f"{datetime.now(tz=UTC) + timedelta(days=14)=}, "
            f"{key=}\n{self._cache=}"
        )
        raise AssertionError(msg)
    return (step_year, step_issue)
__call__ 🔗
__call__(year: int, issue: int) -> datetime

Generate release dates for c’t.

Source code in src/berhoel/ctitools/ct.py
def __call__(self, year: int, issue: int) -> datetime:
    """Generate release dates for c't."""
    key = (year, issue)
    diff = timedelta(days=14)
    if key in self._cache:
        return self._cache[key]
    while key > self._issue_max:
        _step_year, _step_issue = self.__add_issues_up(diff, key)
    while key < self._issue_min:
        self.__add_issues_down(diff, key)

    return self._cache[key]

berhoel.ctitools.ix 🔗

Manage entries for iX.

Classes:

  • Ix –

    Prepare iX issue information.

Classes🔗

berhoel.ctitools.ix.Ix 🔗

Prepare iX issue information.

Parameters:

Methods:

  • __call__ –

    Return CTIEntry from data.

Source code in src/berhoel/ctitools/ix.py
def __init__(
    self,
    arg: IssueData,
):
    """Add information for a iX issue.

    Args:
        arg: Date of issue to work on.
    """
    self.shorttitle: str | None
    if arg.shorttitle is not None:
        self.shorttitle, self.title = arg.shorttitle, arg.title
    else:
        self.shorttitle, self.title = None, arg.title
    self.date: datetime
    self.issue: str
    self.issue, self.date = (
        {
            2016: ("iX Special 2016", DATETIME(2016, 6, 3)),
            2017: ("iX Special 2017", DATETIME(2017, 6, 9)),
            2018: ("iX Special 2018", DATETIME(2018, 5, 25)),
            2019: ("iX Special 2019", DATETIME(2019, 6, 3)),
            2020: ("iX Special 2020", DATETIME(2020, 6, 12)),
            2021: ("iX Special 2021", DATETIME(2021, 6, 9)),
            2022: ("iX Special Green IT", DATETIME(2022, 6, 8)),
            2023: ("iX Special 2023 Künstliche Intelligenz", DATETIME(2023, 6, 13)),
            2024: ("iX Special 2024 Notfallguid", DATETIME(2024, 6, 14)),
        }[arg.year]
        if arg.issue > Ix.last_issue_of_year
        else (Ix.MONTH_ISSUE_MAP[arg.issue], DATETIME(arg.year, arg.issue, 1))
    )
    self.full_issue = f"{arg.year} / {arg.issue}"
    self.author = arg.author
    self.pages = arg.pages
    self.info = arg.info
    self.references = arg.references
    self.keywords = arg.keywords
Functions🔗
__call__ 🔗
__call__() -> CTIEntry

Return CTIEntry from data.

Returns:

Source code in src/berhoel/ctitools/ix.py
def __call__(self) -> CTIEntry:
    """Return CTIEntry from data.

    Returns:
        CTIEntry
    """
    return CTIEntry(
        self.shorttitle,
        self.title,
        self.author,
        self.pages,
        self.full_issue,
        self.info,
        "iX",
        self.date.strftime("%Y-%m-%d"),
        self.references,
        self.keywords,
    )

berhoel.ctitools.ctientry 🔗

Base class for cti (c’t iX) entries.

Classes:

  • CTIEntry –

    Store information from input file.

Classes🔗

berhoel.ctitools.ctientry.CTIEntry dataclass 🔗

CTIEntry(
    shorttitle: str | None,
    title: str,
    author: tuple[str, ...] | None,
    pages: int,
    issue: str,
    info: PaperInfo,
    journaltitle: str,
    date: str,
    references: str,
    keywords: str,
)

Store information from input file.

Methods:

  • __hash__ –

    Calculate hash value for instance.

Functions🔗
__hash__ 🔗
__hash__() -> int

Calculate hash value for instance.

Returns:

  • int –

    hash value

Source code in src/berhoel/ctitools/ctientry.py
def __hash__(self) -> int:
    """Calculate hash value for instance.

    Returns:
      hash value
    """
    return hash(
        (
            self.shorttitle,
            self.title,
            self.author,
            self.pages,
            self.issue,
            self.info,
            self.journaltitle,
            self.date,
            self.references,
            self.keywords,
        ),
    )

berhoel.ctitools.__main__ 🔗

Main code for module.

Functions:

  • main –

    Execute reading of CTI file.

Classes🔗

Functions🔗

berhoel.ctitools.__main__.main 🔗

main() -> None

Execute reading of CTI file.

Source code in src/berhoel/ctitools/__main__.py
def main() -> None:
    """Execute reading of CTI file."""
    parser = argparse.ArgumentParser("Read cti file.")
    parser.add_argument("cti", type=Path, help="input file (required)")
    args = parser.parse_args()

    CTI(args.cti)

berhoel.ctitools.cti2bibtex 🔗

Export cti as BiBTeX for Zotero.

Modules:

  • __main__ –

    Main code for module.

  • tests –

    Tests for ctitools.cti3bibtex.

Classes:

Functions:

  • cti2bibtex –

    Read a cti file and generate a BiBTeX file.

Classes🔗

berhoel.ctitools.cti2bibtex.BiBTeXEntry 🔗

BiBTeXEntry(entry: Ct | Ix | CTIEntry)

Represent BiBTeX entry.

Parameters:

Methods:

  • __str__ –

    Return string for entry.

  • fix_title –

    Prepare string for BiBTeX file.

Source code in src/berhoel/ctitools/cti2bibtex/__init__.py
def __init__(self, entry: Ct | Ix | CTIEntry):
    """Intitalize.

    Args:
        entry: Entry das from CTI file
    """
    self.entry = entry if isinstance(entry, CTIEntry) else entry()
Functions🔗
__str__ 🔗
__str__() -> str

Return string for entry.

Source code in src/berhoel/ctitools/cti2bibtex/__init__.py
    def __str__(self) -> str:
        """Return string for entry."""
        if self.entry.author is None:
            authors = ""
        else:
            authors = " and ".join(
                ", ".join(j[::-1] for j in i[::-1].split(maxsplit=1))
                for ii in self.entry.author
                if (i := (ii if ii is not None else " "))
            )
        papershort = {"c't magazin für computertechnik": "c't"}.get(
            self.entry.journaltitle,
            self.entry.journaltitle,
        )
        keywords = ",".join(
            s for i in self.entry.keywords.split(",") if (s := i.strip())
        )
        res = f"""\
@article{{{self.entry.pages}:{papershort}_{self.entry.issue.replace(" ", "_")},
  title = {{{self.fix_title(self.entry.title)}}},"""
        if self.entry.shorttitle is not None:
            res = f"""{res}
  shorttitle = {{{self.fix_title(self.entry.shorttitle)}}},"""
        return f"""{res}
  author = {{{authors}}},
  date = {{{self.entry.date}}},
  journaltitle = {{{self.entry.journaltitle}}},
  pages = {{{self.entry.pages}}},
  issue = {{{self.entry.issue}}},
  keywords = {{{keywords}}},
}}
"""
fix_title staticmethod 🔗
fix_title(inp: str) -> str

Prepare string for BiBTeX file.

Parameters:

  • inp 🔗 (str) –

    BiBTeX entry sting to modify.

Returns:

  • str –

    BiBTeX entry string wich escaped upper characters

Source code in src/berhoel/ctitools/cti2bibtex/__init__.py
@staticmethod
def fix_title(inp: str) -> str:
    """Prepare string for BiBTeX file.

    Args:
        inp: BiBTeX entry sting to modify.

    Returns:
        BiBTeX entry string wich escaped upper characters
    """
    upper = re.compile(r"([A-Z])")
    return upper.sub(r"{\1}", inp)

Functions🔗

berhoel.ctitools.cti2bibtex.cti2bibtex 🔗

cti2bibtex(
    cti: Path, bibtex: Path, limit_year: int, limit_issue: int, limit_journal: str
) -> None

Read a cti file and generate a BiBTeX file.

CTI: input file, cti, frm, or zip file containing one of the previous (required)

BIBTEX: output file (name will be derived from input file, if not given)

Source code in src/berhoel/ctitools/cti2bibtex/__init__.py
@click.command()
@click.argument("cti", type=Path, nargs=1)
@click.argument("bibtex", type=Path, nargs=1, default=None, required=False)
@click.option(
    "--limit-year",
    type=int,
    default=None,
    help="limit output to given year (default: all years in input file)",
)
@click.option(
    "--limit-issue",
    type=int,
    default=None,
    help="limit output to given issue (default: all issues in input file)",
)
@click.option(
    "--limit-journal",
    type=click.Choice(Papers, case_sensitive=False),
    default=None,
    help="limit output to given magazine ('ix' for iX, or 'ct'  for c't) "
    "(default: both magazines)",
)
@click.version_option(version=f"{metadata.version('ctitools')}")
def cti2bibtex(
    cti: Path, bibtex: Path, limit_year: int, limit_issue: int, limit_journal: str
) -> None:
    """Read a cti file and generate a BiBTeX file.

    `CTI`: input file, cti, frm, or zip file containing one of the  previous
         (required)

    `BIBTEX`: output file (name will be derived from input file, if not given)
    """
    cti_data = CTI(
        cti,
        limit_year,
        limit_issue,
        limit_journal,
    )

    out = cti.with_suffix(".bib") if bibtex is None else bibtex

    with out.open("w") as outp:
        for entry in cti_data:
            outp.write(str(BiBTeXEntry(entry)))

berhoel.ctitools.cti2bibtex.__main__ 🔗

Main code for module.

Functions🔗