texmodel
Declare a LaTeX document family once in Python types; get plain, readable, git-trackable .sty source out of it. The output
is ordinary TeX: commit it, diff it, compile it with whatever LaTeX setup you already have.
Status: 0.1.0, alpha. The public API may change before 1.0. Release migrations,
texmodel-printand the trust and sandbox layers are planned, not built (see the plan).
pip install texmodel # the library and the texmodel-build command
pip install 'texmodel[proof-pdf]' # plus the proof-pdf verifier
One distribution, two packages:
| Package | What it is |
|---|---|
texmodel (src/) |
The library: declare components, render deterministic TeX, check that committed output is current. No TeX needed, no processes spawned. |
texmodel_build (command texmodel-build) |
Optional: a thin latexmk driver with typed log diagnostics, and verifier plugins for built PDFs. Use it if you want compile results as data; your own latexmk or image works just as well. |
texmodel
# acme_cards/__init__.py
from texmodel.body import TemplateRef
from texmodel.components import Command, Environment, Value
from texmodel.package import Package
from texmodel.types import CMYK, Colour, Length, Text
pkg = Package("acme-cards", prefix="ac", version="1.4.0", date="2026/10/21")
@pkg.value
class FactGap(Value[Length]):
"""Space under each fact."""
default = Length("2mm")
@pkg.value
class Ink(Value[Colour]):
"""The text colour."""
default = CMYK(0, 0, 0, 0.95)
@pkg.value
class Compact(Value[bool]):
"""Tighten the layout."""
default = False
@pkg.command
class Fact(Command):
"""One short sentence under the title."""
text: Text
body = TemplateRef("fact.tex") # explicit: a file next to this module; nothing is found by name
@pkg.command
class Slot(Command):
"""A labelled paragraph; the label prints as its heading."""
text: Text
label: Text | None = None # optional: keyword-only, TeX's one optional argument
@pkg.environment
class CardBack(Environment):
"""The back face of a card."""
children = [Name, Fact.times(2, 3), Slot.times(3)] # in order: Name once, Fact 2 to 3, Slot 0 to 3; also X.times.any(), X.times.at_least_one()
begin = r"\begin{minipage}{\linewidth}"
end = r"\end{minipage}"
A body is TeX you point at: a TemplateRef("fact.tex") (relative to the defining module), a string, or a body
method returning either (dedented, so a raw triple-quoted string reads as it renders). #text is the argument named text:
\par\noindent{\color{ac-ink}#text}\par\vspace{\acFactGap}
\ifacCompact\vspace{-\acFactGap}\fi
A Value[T] needs no body: its type decides the declaration (Length -> \newlength + \setlength, Colour ->
\definecolor named ac-ink, Bool -> \newif), and its default is an instance of T. Packages a value needs
(xcolor for colours) are \RequirePackaged once, sorted. A body that names an undeclared \acThing is refused when
render() / texmodel check runs (not at registration, since a body may name a component declared after it).
A body may also be a TexRaw or a sequence or generator of pieces (strings, TemplateRefs, TexRaws), each with its own
Whitespace policy: TRIM writes the % at line ends for you, KEEP (the default, set per package with
Package(whitespace=)) writes lines as given.
from texmodel.body import TexRaw, Whitespace
def body(self):
yield TexRaw(r"""
\par\noindent
{\color{ac-ink}#text}
""", whitespace=Whitespace.TRIM) # \par\noindent% {\color{ac-ink}#1}%
yield r"\par"
A body can call another component by Python binding. The call is checked against the target's arguments, written with the
target's prefix, and a target in another package makes that package a \RequirePackage of the generated .sty:
from texmodel.body import Hole
class Entry(Command):
"""A fact with a rule above it."""
text: Text
def body(self):
yield r"\par\hrule"
yield Fact.call(Hole("text")) # \bkFact{#1}; Fact belongs to base-kit, so \RequirePackage{base-kit}
class Section(Environment):
"""A titled panel."""
begin = Panel.open(title=Hole("title")) # \begin{bkPanel}{#1}
end = Panel.close() # \end{bkPanel}
title: Text
Fact.call(...) takes the required arguments positionally and the optional one by keyword (Slot.call(Hole("t"), label="x"));
an environment's arguments are all keywords. A target must be registered before the body that calls it.
An environment has begin and end in the same way. A document is plain
Python; Text fields are escaped, a Raw field passes TeX through, and everything wrong is reported at once:
tex = pkg.document(
CardBack(Name("Rex"), Fact("Friendly & loyal."), Fact("50% of $5")),
settings=[FactGap(Length("3mm"))], # values for the whole document
options=["a4paper"],
)
CardBack(Fact("x")).validate() # [Violation(TM-C002 ...), Violation(TM-C001 path='CardBack/Fact[0]' ...)]
# pyproject.toml
[tool.texmodel]
package = "acme_cards:pkg"
output = "generated"
A pointer (package =, and the argument of texmodel render) is dotted.module:object, dotted.module:namespace.object
(attributes are followed) or dotted.module:getter() (called with no arguments; its result is used):
package = "acme_cards.decks:Decks.birds" # or "acme_cards.decks:make_package()"
texmodel build # writes generated/acme-cards.sty
texmodel render docs:card # writes card.tex from `card = pkg.document(...)` (or a function returning one); -o to choose
texmodel check # exit 1 if a committed generated file is stale, missing or orphaned; a stale line names its component
# (`changed generated/acme-cards.sty Fact: ...`)
% generated/acme-cards.sty (excerpt)
\RequirePackage{xcolor}
% Space under each fact.
% from acme_cards.FactGap (value: Length)
\newlength{\acFactGap}
\setlength{\acFactGap}{2mm}
% The text colour.
% from acme_cards.Ink (value: Colour)
\definecolor{ac-ink}{cmyk}{0,0,0,0.95}
- Deterministic: no timestamps or hashes; the header records the generator version (
generated with texmodel X.Y.Z), socheckcan say "written by 0.1.0, current is 0.2.0". - Checked early: a bad declaration is refused when the class is defined, with a stable code (
TM-M0xx) and the class named: more than one optional argument, a name that is not a TeX control word, a missing or ambiguous body, a#namethat is not a field, an argument the body never uses, unbalanced braces, a name collision. - Typed:
Fact("A soft woof.")type-checks; fields areText,Rawor either| None; values areLength,CMYK/RGB/Gray(components in [0, 1], no conversion between models) orbool.
Generating a body
A body can also be built from pieces that are well-formed by construction, so a generator cannot write an unbalanced brace or
a bad control word. Group(...) is {...}, Seq(...) the same without braces, Cs("par") a control word (letters and @
only, else TM-M027; _ and : too in Syntax.EXPL3), Lit("50% & co") text escaped as a Text field is, and Hole, Ref and Raw go inside. A space
follows a control word when the next text could continue its name. Loops and conditions are plain Python:
class Rating(Command):
'''Says which level it has.'''
level: Level # a Choice whose members are macros: \lvLow, \lvHigh
def body(self):
for member in Level: # one branch per member, chosen when TeX expands the call
yield Seq(Cs("ifx"), Hole("level"), member.value, Group(Lit(member.name)), Cs("fi"))
# \ifx #1\lvLow{LOW}\fi
# \ifx #1\lvHigh{HIGH}\fi
texmodel writes this TeX and never reads TeX it did not write. The pieces are lines in the package's Whitespace policy;
a Ref inside a Group still makes its package a requirement.
expl3 and library blocks
expl3 is a syntax mode and a source kind, not a typed language: texmodel has no vocabulary of \fp_ or \seq_ functions, and the
compile tells you when one is wrong. Syntax (texmodel.body) is LATEX2E (the default) or EXPL3. A body is in one syntax
(TM-M031 if its pieces differ): give each TexRaw, TemplateRef and Cs that is expl3 syntax=Syntax.EXPL3. The generated
definition is then wrapped in \ExplSyntaxOn / \ExplSyntaxOff, no % is added at line ends (a piece's Whitespace is ignored),
and a Lit or a text argument of a Ref writes ~ for a space. A component whose cs has _ or : says
syntax = Syntax.EXPL3.
Package.library(name, source, syntax=Syntax.EXPL3) carries TeX into the .sty verbatim: dedented, blank edges removed, no %
added, #1 and ##1 as written, in registration order among the package's items, under a provenance comment. Its only static
checks are balanced braces (TM-M011) and no \ExplSyntaxOn / \ExplSyntaxOff inside an EXPL3 block (TM-M034). Every
line of the block has its name as its plan path, and a name is used once (TM-M007). source is a string or a TemplateRef.
sizing = pkg.library("sizing", r"""
\fp_new:N \l__cd_size_fp
\newcommand{\cdFitSize}[2]{ \fp_set:Nn \l__cd_size_fp { min ( 14 , max ( 8 , 12 * #2 / #1 ) ) } }
""", syntax=Syntax.EXPL3)
@pkg.command
class FitSize(Command):
'''Work out the title size.'''
defined_by = sizing # declared and callable (FitSize.call(...)), but no \newcommand is rendered
natural: Raw
available: Raw
A component with defined_by has no body (TM-M032) and its block belongs to the same package (TM-M033). Whether TeX
really defines it, with that many arguments, is checked by compiling: texmodel_build.testkit.check_library(compiler, pkg)
builds pkg.test_document(...) with export_probes(pkg) (it writes texmodel:probe:def.<cs>=yes|no and
arity.<cs>=<n> for each such component), compiles it, and returns TM-C010 for a name the block does not define and
TM-C011 for another number of arguments (? is checked for existence only). The arity is the number of # in the macro's
\meaning, so a command defined with \NewDocumentCommand or with an optional first argument reports 0, which
check_exports flags as TM-C011 when more arguments are declared. A block that TeX refuses fails the compile with the block's name as the diagnostic's path.
The PDF Info strings need no expl3 in a document: PdfInfo(Title="...", Author="...") in before_package or after_package
writes each value PDF-escaped through \csname pdfmanagement_add:nnn\endcsname, and needs RequireMetadata in before_class
(TM-C009). A key is letters only (TM-M035).
Standard libraries
texmodel.std has a header for each TeX library a deck loads: latex (the kernel), graphicx, xcolor, geometry,
etoolbox, eso_pic, microtype, hyphenat, anyfontsize, calc, pdfrender, enumitem, adjustbox, tikz,
tcolorbox, fontspec. A header is an adopted package with no prefix whose components name their TeX command with cs:
class IncludeGraphics(Command):
r"""Place a picture: `\includegraphics[width=3cm]{a.png}`."""
cs = "includegraphics" # the TeX name, stated outright; without `cs` it is prefix + class name
path: Raw
options: Raw | None = None # TeX's one optional argument, written first
A body that calls one (IncludeGraphics.call(Hole("path"), options=Raw(r"width=\linewidth"))) makes the generated .sty
load the package; the kernel header (builtin=True) never does. A library whose commands no body calls, or that takes
options, is loaded with pkg.require(microtype, options=["protrusion=true"]): \RequirePackage[protrusion=true]{microtype},
in that order, before the packages the bodies need. tests/test_std compiles a call of every declared name against the
reference image, so a header that names a command TeX does not have fails, with the path of the call. A header covers
required {} arguments and one leading []; a library's own syntax (TikZ paths such as \draw (0,0) -- (1,1);, key-value
declarations) is written as Passthrough or Raw.
A package that is already written in TeX
When the .sty is hand-written, declare what it defines instead of generating it: adopted=True (M9). The components
have no body, build writes no file, documents are still checked and escaped, and texmodel check compares each
declaration with the TeX it names (\newcommand, \providecommand and \newenvironment are read; a name defined any
other way counts as missing):
pkg = Package("card-deck", prefix="cd", adopted=True) # the package name is the hand-written one
@pkg.command
class TalkAboutMe(Command):
"""A question, with an optional answer."""
question: Text
answer: Text | None = None # declared [2][]: \newcommand{\cdTalkAboutMe}[2][]{...}
@pkg.command
class DeckName(Command): # no fields: a setting the deck sets with \renewcommand
"""The deck name printed on every card."""
[tool.texmodel]
package = "deck.model:pkg"
sources = ["res/brand/*.sty"] # globs, relative to pyproject.toml
texmodel check # TM-C010: declared, not defined; TM-C011: defined with another signature
Package.document(..., preamble=PreambleSlots(before_class=, before_package=, after_package=)) places Raw TeX where the
document needs it: before_class for \DocumentMetadata, before_package for \input@path, after_package for
\graphicspath and per-document \renewcommands. A public name of the package used there (\cdSafeInset) that no
component declares is refused with its path (TM-M015, preamble.after_package[0]). Typed entries render to one Raw and know
their slot: PassOption("xcolor", "table"), InputPath("res/") (before_package), GraphicsPath("images/") (after_package,
the current directory first unless include_cwd=False) and RequireMetadata(lang="en-GB") (before_class); one in another
slot is TM-C008. tests/consumers/card_deck/ is a
complete example: a hand-written package, the generator it replaces and a 3-card deck.
A text setting
@pkg.value
class DeckName(Value[Text]):
"""The deck name printed on every card."""
default = "Untitled" # an adopted package may leave it out: the hand-written TeX has it
pkg.document(card, settings=[DeckName("R&D")]) # \renewcommand{\acDeckName}{R\&D}, after \usepackage
A generated package defines \newcommand{\acDeckName}{Untitled}; for an adopted one texmodel check expects the hand-written
package to define the macro with no arguments. Length, colour and flag settings are written in the document body as before.
A Value[Raw] is the same for a setting that is TeX (\newcommand{\acFooter}{\textit{draft}}, renewed by settings=[Footer(Raw(...))]).
A PdfString field holds text for the inside of a PDF literal string you write yourself (\pdfextension info{/Subject (#text)}):
it is written as \detokenize{...} with ( ) \ and every TeX special as an octal escape (\050), and text with a non-ASCII
character as UTF-16BE, so a parenthesis cannot cut the string short, % or _ cannot break the TeX, and a reader gets the
text back. It is not for \hypersetup{pdftitle=...}: hyperref escapes its own input.
Layout that is not a component
class Page(Environment):
'''A page.'''
children = [Name, Anywhere(Heading), Fact.times(2, 3), Passthrough.times.any()]
Page(Name("Rex"), Heading("Facts"), Fact("a"), Passthrough(Raw(r"\vspace{1mm}")), Fact("b"))
Anywhere(Heading) allows zero or more of that component anywhere among the other children; it is not counted against the
ordered terms and the kind cannot also be an ordered term (TM-M029). Passthrough(Raw(...)) writes its TeX as it is and is
accepted only where a grammar lists it.
A closed set of values
A Choice is an enum whose members carry their TeX: a Raw passes through, a str is escaped. Use it as a field type
(optionally | None); data names a member by its name.
@enum.unique
class SlotLabel(Choice):
SOUND = Raw(r"\deckParamSound")
WHERE = Raw(r"\deckParamWhere")
@pkg.command
class LabelledSlot(Command):
"""A fact with a label."""
label: SlotLabel
text: Text
LabelledSlot(SlotLabel.SOUND, "A soft woof.") # \cdLabelledSlot{\deckParamSound}{A soft woof.}
A document from data
texmodel.spec loads a document from plain data (what TOML or JSON gives), using pydantic: a child names its component with
"type", an environment's children are its content, a Raw field takes a string.
from texmodel.spec import from_dict, json_schema, to_dict
card = from_dict(Card, {"content": [
{"type": "CardFront", "content": [{"type": "CardImage", "path": "a.jpg"}]},
{"type": "CardBack", "content": [{"type": "Name", "text": "Fox"}, {"type": "LeadFact", "text": "..."}]},
]})
card.validate() # the grammar (counts, order) is checked here, as for any tree
to_dict(card) # the same shape back
json_schema(Card) # JSON Schema of what from_dict accepts
Everything wrong comes at once as an ExceptionGroup of ValidationError (TM-C020), each with a path such as
Card/CardBack[1]/LeadFact[2].text.
A schema for the package's data
texmodel schema writes json_schema(pkg) to [tool.texmodel] schema_output: one JSON Schema with a $defs entry for every
command and environment (a child is told apart by its "type" constant) and Value.<Name> for every value, for editors and
validators of the TOML or JSON that from_dict reads. A renamed field keeps loading under its old key:
class Fact(Command):
'''One fact.'''
text: Text = field(renamed_from="body") # {"body": "..."} loads; to_dict writes "text"; the schema has x-renamed-from
Checking committed documents
[tool.texmodel.documents]
"deck.tex" = "deck.build:plan()" # committed file = pointer to a Plan (or a str) the model renders
texmodel check renders each pointer from today's model and compares it with the committed file, like terraform plan:
for a Plan every difference names the component that writes the line (changed deck.tex Card[1]/CardBack[1]/LabelledSlot[3]: ...),
so a hand edit and a stale render read the same; a str is compared whole. Exit 1 on any difference; texmodel render with no
pointer rewrites every configured document.
Comments
from texmodel.body import Comment
class CardHeader(Comment):
"""The audit block above a card."""
template = """
Card: #name
id: #id
"""
name: str
id: str
pkg.document(CardHeader("Red Fox", "11111111-aaaa"), Card(...)) # % Card: Red Fox / % id: 11111111-aaaa
Every line of a Comment is prefixed % , so a value cannot leave it; nothing is escaped (## is a # in a template). It
is a body piece (yield Comment("why this is 3mm")) and a document node: a child of an environment or a top-level node, and
transparent to the grammar (not counted, never listed, paths keep the real index: Card[3]/CardHeader[0]). It needs no
package registration. plan.edits(text, ignore_comments=True) skips lines that are comments on both sides;
from_dict(cls, data, comments=[CardHeader]) and json_schema(cls, comments=[...]) know it as a child ("type": "CardHeader").
Testing generated code with stubs
A component's body decides what it passes on to other components. expand(node, stubs=[Fact]) (texmodel.testing) walks the
bodies with the node's values and returns the body text and a Call for every stubbed component, in order, with no TeX:
from texmodel.testing import Call, expand
section = Section(Entry("First"), Entry("R&D 50%"), title="Notes")
assert [c for c in expand(section, stubs=[Fact]) if isinstance(c, Call)] == [
Call("bkFact", ("First",)), Call("bkFact", (r"R\&D 50\%",))]
The TeX tier checks the real macros do the same: pkg.test_document(section, stub=[Fact]) loads the real packages and replaces
each stub with a macro that writes texmodel:probe:callN=bkFact{...} to the log; texmodel_build.testkit.run_probes(compiler, tex, files={"base-kit.sty": ...}) compiles it and returns the probes, which Call.parse reads. Arguments are shown as
\detokenize shows them (a space after each control word), so both tiers agree. Layout is not covered: this tests what a
component passes on, not how it looks.
Where a line came from
pkg.plan(...) takes the arguments of pkg.document(...) and returns a Plan: every output line with the path of what wrote it
(Card[1]/CardBack[1]/LabelledSlot[3], preamble.after_package[0]). plan.text is the document. Store plan.to_json() beside
the committed file; Plan.from_json(saved).edits(text) then names each hand edit by the path it had when it was written, even
if the model has changed since, and plan.path_of(39) names the component behind a log's input line 39. Compiler.compile(src, out, plan=plan) does
that for every diagnostic in the source: diagnostic.path is the component behind its line; plans={"acme.sty": pkg.render_plan()} does the
same for diagnostics in other files, by file name.
texmodel.bundle copies a manifest of files and refuses if any is missing (stale compares bytes, not timestamps);
texmodel.drift is what check uses; texmodel.pointer resolves module:object pointers (a package, a document, a Plan).
texmodel-build
texmodel-build doctor # tools on this machine, and what the configured image has
texmodel-build doctor --sty 'res/*.sty' # which files those packages load that the toolchain lacks (kpsewhich), with a tlmgr line; exit 3
texmodel-build compile doc.tex # exit 0 ok, 1 errors in the document, 3 tool or image missing
texmodel-build compile doc.tex --json --epoch 1700000000 --root generated
texmodel-build compile deck.tex --plan "deck.build:plan()" # diagnostics name the component: [Card[1]/CardBack[1]/Name[0]]
texmodel-build build -j 4 # every [tool.texmodel-build.targets.*]
texmodel-build verify out/*.pdf # verifier plugins; anything unconfirmable fails
[tool.texmodel-build]
image = "releaseart/lualatex-python:latest" # runs `docker run` over it; omit to run on this machine
-
The compile log becomes typed diagnostics with stable codes: undefined control sequences (
TM-X010), missing glyphs (TM-X011), overfull boxes (TM-X012), missing files (TM-X013), package errors (TM-X014), undefined references (TM-X018), package warnings with their continuation lines (TM-X019, how a hand-written package says a card does not fit), non-convergence with the.auxentries that keep moving (TM-X030), and a fallback so no failure is silent. -
A package's "does not fit" warning becomes a typed
Overset(TM-X031, an error: item, needs, has, over, in pt) when a rule says how to read it; the warning is kept too. A package that printstexmodel:probe:fit.<item>=<needs>/<has>needs no rule. Lengths takept,mm,cm,in(a bare number is pt); an item that fits gives none.[tool.texmodel-build.measures.card-fit] package = "card-deck" pattern = "Card '(?P<item>[^']+)' does not fit. Its body needs (?P<needs>\\S+) and the card has (?P<has>\\S+)"
-
Reproducibility is the image's job (
--epochpinsSOURCE_DATE_EPOCH); there is no sandbox or cache here. -
texmodel-build verifyruns plugins from thetexmodel_build.verifiersentry-point group.pip install 'texmodel[proof-pdf]'adds proof-pdf. A verifier that cannot run, a file that cannot be opened and a profile that selects nothing are failures; the only excuse is a declaredexemptglob. -
The image must contain
latexmk(releaseart/lualatex-pythonhas it since proofpress v0.3.4);doctorreports it if not.
Where it is going
Open work is in the plan (§19, §21a) and the roadmap.
Development
See CONTRIBUTING.md. bin/autoformat.sh runs ruff, strict pyright and import-linter over both packages;
bin/test.sh runs pytest with a 100% coverage gate. The container compile suite is pytest -m tex; it skips without an
image that has latexmk, and TEXMODEL_REQUIRE_TEX=1 turns that skip into a failure (TEXMODEL_TEST_IMAGE sets the image).
bin/test-image.sh builds texmodel-test from docker/test/Dockerfile: the published reference image plus the few TeX
packages the suite needs that it does not carry yet (TEXMODEL_TEST_IMAGE=texmodel-test pdm run pytest -m tex); CI uses it.
Public API is the top-level modules of each package; everything under _internal may change. Report vulnerabilities as
described in SECURITY.md.
Licence
MIT. The reference images carry their own licences (Ghostscript is AGPL); texmodel-build runs them as a
separate process and ships none of their code.
Metadata
Release files for texmodel 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| texmodel-0.2.0.tar.gz | 182.6 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| texmodel-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 287.8 kB
Release files / texmodel-0.2.0.tar.gz
| Download URL | texmodel-0.2.0.tar.gz |
|---|---|
| Size | 182.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a07b14b173dc5dc6adad37fd93cbb20cb1cdc7c7b73211576f01cb0254f5332d
|
|
BLAKE2b-256 checksum How to use checksums |
712d27427c5bc5f03b6b6beb58db5d452bb567bc4d72f91a903e1b728bafeec5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logRelease files / texmodel-0.2.0-py3-none-any.whl
| Download URL | texmodel-0.2.0-py3-none-any.whl |
|---|---|
| Size | 105.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3ce7d9fe7eed29f59ba144c40aa0119d1f6450752476037922fe6779827ea33e
|
|
BLAKE2b-256 checksum How to use checksums |
2351be2e0b473b445234f606b55ae7ce0fb09fa8f29303fefbd9e5db1e141e7a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logSecurity for texmodel 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for texmodel-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.