texpkg 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
texpkg/document.py ADDED
@@ -0,0 +1,225 @@
1
+ """`Document[T]`: a kind's tree and the directory it lives in (P27, P28)."""
2
+
3
+ import dataclasses
4
+ import datetime
5
+ import enum
6
+ import json
7
+ import pathlib
8
+ from collections.abc import Mapping
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ from texmodel import drift
12
+ from texmodel.components import Command, Environment
13
+
14
+ from . import provenance
15
+ from ._internal import directory, handedits, state
16
+ from .contracts import Pair
17
+ from .declaration import Kind
18
+ from .errors import HandEditError, NotADocumentError
19
+
20
+ if TYPE_CHECKING:
21
+ from ._internal.release import Release
22
+
23
+
24
+ @enum.unique
25
+ class FileState(enum.StrEnum):
26
+ UNCHANGED = "unchanged"
27
+ CHANGED = "changed"
28
+ MISSING = "missing"
29
+ EXTRA = "extra" # in a place the payload writes to, and not written by it: a retired `.sty` (A4)
30
+
31
+
32
+ @enum.unique
33
+ class HandEditKind(enum.StrEnum):
34
+ ADDED = "added" # in the file, not in the plan
35
+ REMOVED = "removed" # in the plan, not in the file
36
+ CHANGED = "changed"
37
+
38
+
39
+ @dataclasses.dataclass(frozen=True)
40
+ class HandEdit:
41
+ """One line of a `.tex` that is not what `save` wrote (D8). `path` is the node's data path (`/content/1/content/0`),
42
+ or the plan's own for a line no node wrote; `value` is the typed value when texmodel can read the edit back (D9),
43
+ else `None`: it is never guessed."""
44
+
45
+ file: pathlib.Path
46
+ path: str
47
+ expected: str
48
+ found: str
49
+ kind: HandEditKind
50
+ value: object = None
51
+
52
+
53
+ @dataclasses.dataclass(frozen=True)
54
+ class TemplateChange:
55
+ """One thing a new release changes for a saved document (D4): a setting a `.tex` is written with
56
+ (`setting deck.tex FactGap`), a default (`default Fact.accent`) or a `.sty` (`sty acme-brand.sty`, by hash).
57
+ `old` and `new` are the recorded and the current value; `None` is absent."""
58
+
59
+ setting: str
60
+ old: object
61
+ new: object
62
+
63
+
64
+ @dataclasses.dataclass(frozen=True)
65
+ class Check:
66
+ """What a `save` would change, and nothing changed to find out (P29, A4). A file that is `EXTRA` is reported, but a
67
+ save removes nothing, so it does not make the document not current."""
68
+
69
+ files: Mapping[pathlib.Path, FileState]
70
+ edits: tuple[HandEdit, ...] = ()
71
+
72
+ @property
73
+ def current(self) -> bool:
74
+ return not self.edits and all(s in (FileState.UNCHANGED, FileState.EXTRA) for s in self.files.values())
75
+
76
+
77
+ @dataclasses.dataclass(frozen=True)
78
+ class StackEntry:
79
+ package: str
80
+ release: str
81
+ output: Pair # what the package rendered with, `current:revision` (V18)
82
+
83
+
84
+ @dataclasses.dataclass(frozen=True)
85
+ class Stack:
86
+ """The packages a document was rendered through, the top one first: its lockfile (V18)."""
87
+
88
+ entries: tuple[StackEntry, ...]
89
+
90
+
91
+ class Document[T: Command | Environment]:
92
+ """A texmodel tree of a kind's classes (`doc.tree`) with texpkg's interface as methods (P28). Made by a kind's
93
+ `load` and `create`, never by hand; a tree is frozen, so `replace` gives another document."""
94
+
95
+ def __init__(self, tree: T, path: pathlib.Path, release: "Release", kind: Kind) -> None:
96
+ self.tree = tree
97
+ self.path = path
98
+ self._release, self._kind = release, kind
99
+
100
+ @property
101
+ def kind(self) -> str:
102
+ return self._kind.name
103
+
104
+ @property
105
+ def defaults(self) -> tuple[str, ...]:
106
+ """The data paths of the values that are still the package's defaults (V21)."""
107
+ return state.default_paths(self.tree)
108
+
109
+ @property
110
+ def stack(self) -> Stack:
111
+ """The packages the document was last saved through; the release's own when nothing is saved yet."""
112
+ saved = directory.read(self.path, directory.TEMPLATE)
113
+ template = json.loads(saved) if saved else self._release.template(self._kind)
114
+ return Stack(tuple(StackEntry(e["package"], e["release"], Pair(*e["output"])) for e in template["stack"]))
115
+
116
+ def render(self, variant: str | None = None) -> str:
117
+ """The `.tex` of the document, complete: pure, nothing is written. Raises an `ExceptionGroup` of texmodel
118
+ `ValidationError` when the tree breaks a rule."""
119
+ return self._release.plan(self._kind, self.tree, variant).text
120
+
121
+ def replace(self, tree: T) -> "Document[T]":
122
+ """The same document, in the same directory, with another tree."""
123
+ return Document(tree, self.path, self._release, self._kind)
124
+
125
+ def _expected(self, stamp: str, time: str) -> dict[str, str | bytes]:
126
+ """Every file a save writes, by path in the directory."""
127
+ files = provenance.files(self._release, stamp, time)
128
+ payload = self._release.payload(self._kind, files)
129
+ return (
130
+ payload.expected()
131
+ | self._release.rendered(self._kind, self.tree)
132
+ | self._release.states(self._kind, self.tree)
133
+ )
134
+
135
+ def _data_path(self, plan_path: str) -> str:
136
+ """A profile's tree is expanded before it is written, so a plan path names a node of the base's tree and not a
137
+ path in this document's data: it is given as it is."""
138
+ return plan_path if self._kind.expand else handedits.data_path(plan_path)
139
+
140
+ def hand_edits(self) -> tuple[HandEdit, ...]:
141
+ """The lines of the `.tex` files that differ from the plan `save` kept of them (D8)."""
142
+ return tuple(
143
+ HandEdit(
144
+ self.path / name,
145
+ self._data_path(edit.path),
146
+ edit.expected,
147
+ edit.found,
148
+ HandEditKind(edit.kind.value),
149
+ )
150
+ for name, edit in handedits.edits(self.path, self._kind.documents())
151
+ )
152
+
153
+ def save(self, *, now: datetime.datetime | None = None, stamp: str | None = None, force: bool = False) -> None:
154
+ """Write the states, the `.tex` of every variant with its plan, and the payload of the kind: only what changed,
155
+ and a history entry only when the states did (P29). Raises `HandEditError`, writing nothing, when a `.tex` was
156
+ edited by hand, unless `force`. Nothing is written if the tree or the payload is not whole.
157
+
158
+ The provenance files carry `stamp` (default: the release's version, V4) and the time of `now` (default: the
159
+ clock, N4). Their time is kept while the stamp and the rest of the payload are as they were."""
160
+ rendered = self._release.rendered(self._kind, self.tree)
161
+ states = self._release.states(self._kind, self.tree)
162
+ wanted = provenance.stamp_of(stamp, self._release)
163
+ bare = self._release.payload(self._kind)
164
+ bare.complete()
165
+ saved = provenance.saved(self.path, self._release)
166
+ unchanged = saved is not None and saved.stamp == wanted and not bare.pending(self.path)
167
+ time = saved.time if unchanged and saved else provenance.time_of(now)
168
+ payload = self._release.payload(self._kind, provenance.files(self._release, wanted, time))
169
+ if not force and (edits := self.hand_edits()):
170
+ files = ", ".join(sorted({e.file.name for e in edits}))
171
+ raise HandEditError(
172
+ f"{len(edits)} hand-edited line(s) in {files}: not overwritten",
173
+ edits=edits,
174
+ hint="doc.hand_edits() lists them; save(force=True) overwrites them",
175
+ )
176
+ payload.write(self.path)
177
+ directory.write(self.path, directory.differing(self.path, rendered))
178
+ if changed := directory.differing(self.path, states):
179
+ directory.rotate_history(self.path)
180
+ directory.write(self.path, changed)
181
+
182
+ def check(self, *, stamp: str | None = None) -> Check:
183
+ """What a save would change; writes nothing. A hand-edited `.tex` is reported as changed and in `edits`. The
184
+ provenance files are compared by stamp (A4): `stamp` is the one the document was saved with (default: the
185
+ release's version), and the time they hold is not a difference."""
186
+ wanted = provenance.stamp_of(stamp, self._release)
187
+ saved = provenance.saved(self.path, self._release)
188
+ expected = self._expected(wanted, saved.time if saved else provenance.time_of(None))
189
+ report = drift.check_drift(expected, self.path, self._release.dist.layout.owned())
190
+ files: dict[pathlib.Path, FileState] = {}
191
+ for rel in expected:
192
+ files[self.path / rel] = (
193
+ FileState.MISSING
194
+ if rel in report.missing
195
+ else FileState.CHANGED
196
+ if rel in report.stale
197
+ else FileState.UNCHANGED
198
+ )
199
+ files |= {self.path / rel: FileState.EXTRA for rel in report.orphaned}
200
+ return Check(files, self.hand_edits())
201
+
202
+ def template_changes(self) -> tuple[TemplateChange, ...]:
203
+ """What the installed release changes for this document against the template state it was saved with (D4)."""
204
+ saved = directory.read(self.path, directory.TEMPLATE)
205
+ if saved is None:
206
+ raise NotADocumentError(
207
+ f"{self.path}: no {directory.TEMPLATE}", code="TP-D001", path=str(self.path), hint="save the document"
208
+ )
209
+ old, new = _flat(json.loads(saved)), _flat(self._release.template(self._kind))
210
+ return tuple(
211
+ TemplateChange(key, old.get(key), new.get(key)) for key in sorted(old | new) if old.get(key) != new.get(key)
212
+ )
213
+
214
+
215
+ def _flat(template: dict[str, Any]) -> dict[str, object]:
216
+ """The parts of a template state a release can change for a document, one entry each."""
217
+ return (
218
+ {
219
+ f"setting {file} {value}": line
220
+ for file, lines in template["settings"].items()
221
+ for value, line in lines.items()
222
+ }
223
+ | {f"default {name}": value for name, value in template["defaults"].items()}
224
+ | {f"sty {name}": digest for name, digest in template["sty"].items()}
225
+ )
texpkg/errors.py ADDED
@@ -0,0 +1,66 @@
1
+ """The errors of a package: a built-in base and a stable `.code` each (B11).
2
+
3
+ No class is shared between packages (each wheel embeds its own copy of this module), so a consumer catches the built-in
4
+ base (`ValueError`, `FileNotFoundError`) and reads `.code`, which is `TP-<area><nnn>` and never reused.
5
+ """
6
+
7
+ from typing import TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ from .document import HandEdit
11
+ from .migration import Unresolved
12
+
13
+
14
+ class TexpkgError(Exception):
15
+ """What every texpkg error carries: a stable code, where it happened and the fix."""
16
+
17
+ def __init__(self, message: str, *, code: str, path: str = "", hint: str = "") -> None:
18
+ super().__init__(message)
19
+ self.code = code
20
+ self.path = path
21
+ self.hint = hint
22
+
23
+
24
+ class DeclarationError(TexpkgError, ValueError):
25
+ """A declaration (an asset, a layout, a kind, a distribution) is invalid."""
26
+
27
+
28
+ class PayloadError(TexpkgError, FileNotFoundError):
29
+ """A payload that cannot be written whole: a source file is absent (A3)."""
30
+
31
+
32
+ class NotADocumentError(TexpkgError, FileNotFoundError):
33
+ """The directory holds no readable document state (P27)."""
34
+
35
+
36
+ class DocumentExistsError(TexpkgError, FileExistsError):
37
+ """`create` into a directory that already holds a document."""
38
+
39
+
40
+ class KindMismatchError(TexpkgError, ValueError):
41
+ """A kind's `load` of a directory that holds another kind's document, or another package's (K3)."""
42
+
43
+ def __init__(self, message: str, *, found: str, expected: str, path: str = "", hint: str = "") -> None:
44
+ super().__init__(message, code="TP-D003", path=path, hint=hint)
45
+ self.found = found
46
+ self.expected = expected
47
+
48
+
49
+ class NewerDataError(TexpkgError, ValueError):
50
+ """Data written for a newer `data` version than this release reads (V14)."""
51
+
52
+
53
+ class MigrationError(TexpkgError, ValueError):
54
+ """Adopting a release failed; nothing was returned or written (V31). `unresolved` lists what no migration placed."""
55
+
56
+ def __init__(self, message: str, *, code: str, unresolved: "tuple[Unresolved, ...]" = (), hint: str = "") -> None:
57
+ super().__init__(message, code=code, hint=hint)
58
+ self.unresolved = unresolved
59
+
60
+
61
+ class HandEditError(TexpkgError, ValueError):
62
+ """`save` found a `.tex` that differs from the plan of what was last written (D8). `edits` lists them."""
63
+
64
+ def __init__(self, message: str, *, edits: "tuple[HandEdit, ...]", hint: str = "") -> None:
65
+ super().__init__(message, code="TP-D005", hint=hint)
66
+ self.edits = edits
texpkg/layout.py ADDED
@@ -0,0 +1,48 @@
1
+ """The layout (A2): where a payload file goes under a document directory, and the preamble lines that find it (A5).
2
+
3
+ Policy over `texmodel.bundle`, which decides nothing about placement. There is one layout, `LAYOUT`, for every
4
+ distribution: a package built on another writes into the same directory, which the one set of preamble lines finds.
5
+ """
6
+
7
+ import dataclasses
8
+ import pathlib
9
+
10
+ from texmodel.package import PreambleSlots
11
+ from texmodel.preamble import GraphicsPath, InputPath
12
+
13
+ from .assets import Role
14
+ from .errors import DeclarationError
15
+
16
+
17
+ @dataclasses.dataclass(frozen=True)
18
+ class Project:
19
+ """`.sty`, graphics and notes in `dir`; fonts at the root, since luaotfload reads no document-side search path;
20
+ `BY_PATH` at its own path."""
21
+
22
+ dir: pathlib.PurePosixPath
23
+
24
+ def __post_init__(self) -> None:
25
+ if not self.dir.parts or self.dir.is_absolute() or ".." in self.dir.parts or "." in self.dir.parts:
26
+ raise DeclarationError(
27
+ f"layout directory {str(self.dir)!r}: not a relative directory inside the document",
28
+ code="TP-A003",
29
+ hint="e.g. Project(PurePosixPath('res/acme'))",
30
+ )
31
+
32
+ def place(self, role: Role, path: pathlib.PurePosixPath) -> pathlib.PurePosixPath:
33
+ match role:
34
+ case Role.BY_PATH:
35
+ return path
36
+ case Role.FONT:
37
+ return pathlib.PurePosixPath(path.name)
38
+ case Role.PACKAGE | Role.GRAPHIC | Role.NOTE:
39
+ return self.dir / path.name
40
+
41
+ def preamble(self) -> PreambleSlots:
42
+ return PreambleSlots(before_package=(InputPath(f"{self.dir}/"),), after_package=(GraphicsPath(f"{self.dir}/"),))
43
+
44
+ def owned(self) -> tuple[str, ...]:
45
+ return (f"{self.dir}/**/*",)
46
+
47
+
48
+ LAYOUT = Project(pathlib.PurePosixPath("res/texpkg"))
texpkg/migration.py ADDED
@@ -0,0 +1,73 @@
1
+ """Migrations: one API whoever writes them (V32): the build derives some, a kind lists some, a consumer passes some.
2
+
3
+ A migration is a function over plain data, from the stored state of `data` version `to - 1` to `to` (V28); what it
4
+ cannot place it leaves unresolved, never guessed (V15).
5
+ """
6
+
7
+ import dataclasses
8
+ import enum
9
+ from collections.abc import Callable, Sequence
10
+ from typing import Any
11
+
12
+ from .errors import DeclarationError
13
+
14
+
15
+ @enum.unique
16
+ class DefaultChange(enum.StrEnum):
17
+ """What a consumer does when a release changes a default that a stored document still marks as default (V23). A
18
+ `StrEnum`, so one package's value equals another's (B11)."""
19
+
20
+ FOLLOW = "follow" # the marked value moves to the new default
21
+ FREEZE = "freeze" # it keeps the old value, which stops being marked
22
+ ASK = "ask" # adoption aborts with each changed default as an unresolved item
23
+
24
+
25
+ @dataclasses.dataclass(frozen=True)
26
+ class Unresolved:
27
+ """What no migration placed: the data path, why, and the old value. texpkg fills `schema` (the new schema at that
28
+ path) and `boundary` (`"4 -> 5"`) when it reports."""
29
+
30
+ path: str
31
+ reason: str
32
+ value: object = None
33
+ schema: dict[str, Any] = dataclasses.field(default_factory=dict[str, Any])
34
+ boundary: str = ""
35
+
36
+
37
+ @dataclasses.dataclass(frozen=True)
38
+ class Migrated:
39
+ """A migration's result: the state, and what it left unresolved."""
40
+
41
+ state: dict[str, Any]
42
+ unresolved: Sequence[Unresolved] = ()
43
+
44
+
45
+ type MigrationFunction = Callable[[dict[str, Any]], Migrated]
46
+
47
+
48
+ @dataclasses.dataclass(frozen=True)
49
+ class Migration:
50
+ """A function from `data` version `to - 1` to `to`; make one with `@migration(to=N)`."""
51
+
52
+ to: int
53
+ function: MigrationFunction
54
+
55
+ def __post_init__(self) -> None:
56
+ if self.to < 2:
57
+ raise DeclarationError(
58
+ f"migration(to={self.to}): `data` version 1 is the first, so a migration goes to 2 or later",
59
+ code="TP-M001",
60
+ hint="@migration(to=N) migrates from `data` version N - 1 to N",
61
+ )
62
+
63
+ def __call__(self, state: dict[str, Any]) -> Migrated:
64
+ return self.function(state)
65
+
66
+
67
+ def migration(*, to: int) -> Callable[[MigrationFunction], Migration]:
68
+ """`@migration(to=5)`: from `data` version 4 to 5."""
69
+
70
+ def wrap(function: MigrationFunction) -> Migration:
71
+ return Migration(to, function)
72
+
73
+ return wrap
texpkg/protocol.py ADDED
@@ -0,0 +1,61 @@
1
+ """The fixed surface of a built package (P6), as protocols: the build generates a module to satisfy them and checks it
2
+ does, and a consumer's type checker reads the generated module's own types."""
3
+
4
+ import datetime
5
+ import pathlib
6
+ from collections.abc import Mapping, Sequence
7
+ from typing import Any, Literal, Protocol, runtime_checkable
8
+
9
+ from .contracts import Changes, Contracts
10
+ from .document import Document
11
+ from .migration import DefaultChange, Migration
12
+
13
+
14
+ @runtime_checkable
15
+ class KindModule(Protocol):
16
+ """`acme_tex.kinds.expert`: besides these, the kind's own classes (P24)."""
17
+
18
+ @property
19
+ def NAME(self) -> str: ... # noqa: N802
20
+ def load(
21
+ self,
22
+ path: pathlib.Path | str,
23
+ *,
24
+ migrations: Sequence[Migration] = (),
25
+ default_changes: DefaultChange | Mapping[str, DefaultChange] = DefaultChange.FOLLOW,
26
+ ) -> Document[Any]: ...
27
+ def create(self, path: pathlib.Path | str, tree: Any) -> Document[Any]: ... # noqa: ANN401 # the kind's root class
28
+ def schema(self, mode: Literal["validation", "serialization"] = "validation") -> dict[str, Any]: ...
29
+ def example(self) -> Any: ... # noqa: ANN401 # the kind's root class
30
+ def variants(self) -> tuple[str, ...]: ...
31
+ def changes(self, old: int, new: int) -> Changes: ...
32
+
33
+
34
+ @runtime_checkable
35
+ class KindsModule(Protocol):
36
+ """`acme_tex.kinds`."""
37
+
38
+ def names(self) -> tuple[str, ...]: ...
39
+ def modules(self) -> Mapping[str, KindModule]: ...
40
+
41
+
42
+ @runtime_checkable
43
+ class PackageModule(Protocol):
44
+ """`acme_tex`."""
45
+
46
+ @property
47
+ def __version__(self) -> str: ...
48
+ @property
49
+ def CONTRACTS(self) -> Contracts: ... # noqa: N802
50
+ @property
51
+ def kinds(self) -> KindsModule: ...
52
+ def export(
53
+ self, dest: pathlib.Path | str, *, now: datetime.datetime | None = None, stamp: str | None = None
54
+ ) -> tuple[pathlib.Path, ...]: ...
55
+ def load(
56
+ self,
57
+ path: pathlib.Path | str,
58
+ *,
59
+ migrations: Sequence[Migration] = (),
60
+ default_changes: DefaultChange | Mapping[str, DefaultChange] = DefaultChange.FOLLOW,
61
+ ) -> Document[Any]: ...
texpkg/provenance.py ADDED
@@ -0,0 +1,61 @@
1
+ """The provenance file: what a document directory was materialised from and when (V2, V34).
2
+
3
+ `save` writes `<package>-provenance.sty` for every package of the stack, the only payload file that holds a time. A
4
+ document can print what it was built from with `\\<prefix>ProvenanceInfo`, and the PDF carries the same text.
5
+ """
6
+
7
+ import dataclasses
8
+ import datetime
9
+ import pathlib
10
+ from typing import TYPE_CHECKING
11
+
12
+ from ._internal import directory, provenance_tex
13
+ from .errors import DeclarationError
14
+
15
+ if TYPE_CHECKING:
16
+ from ._internal.release import Release
17
+
18
+
19
+ @dataclasses.dataclass(frozen=True)
20
+ class Provenance:
21
+ stamp: str
22
+ time: str
23
+
24
+
25
+ def stamp_of(stamp: str | None, release: "Release") -> str:
26
+ """The stamp a save or a check uses: the consumer's (V4, e.g. `1.2.0-3f9c2ab-dirty`), else the release's version."""
27
+ chosen = release.version if stamp is None else stamp
28
+ if not provenance_tex.STAMP.fullmatch(chosen):
29
+ raise DeclarationError(
30
+ f"stamp {chosen!r}: letters, digits and . + : / @ - and space only",
31
+ code="TP-D006",
32
+ hint="it is printed on the page and written into the PDF's metadata, so it has to be the same text in both",
33
+ )
34
+ return chosen
35
+
36
+
37
+ def time_of(now: datetime.datetime | None) -> str:
38
+ """`now` in UTC, or the clock's. This is the one place texpkg reads the clock (N4): a caller that wants the
39
+ time pinned passes `now`."""
40
+ moment = now or datetime.datetime.now(datetime.UTC)
41
+ if moment.tzinfo is not None:
42
+ moment = moment.astimezone(datetime.UTC)
43
+ return moment.strftime("%Y-%m-%dT%H:%M:%SZ")
44
+
45
+
46
+ def files(release: "Release", stamp: str, time: str) -> dict[str, str]:
47
+ """The provenance file of every package in the stack, by file name."""
48
+ return {
49
+ provenance_tex.file_name(r.dist.package.name): provenance_tex.companion(
50
+ r.dist.package.prefix, r.dist.package.name, r.version, stamp, time
51
+ )
52
+ for r in release.releases()
53
+ }
54
+
55
+
56
+ def saved(root: pathlib.Path, release: "Release") -> Provenance | None:
57
+ """The stamp and time of the top package's provenance file in `root`; `None` if it is not there."""
58
+ text = directory.read(root, release.layout_path(provenance_tex.file_name(release.dist.package.name)))
59
+ if text is None:
60
+ return None
61
+ return Provenance(*provenance_tex.read(text, release.dist.package.prefix))
texpkg/py.typed ADDED
File without changes
@@ -0,0 +1,30 @@
1
+ Metadata-Version: 2.4
2
+ Name: texpkg
3
+ Version: 0.1.0
4
+ Summary: LaTeX package management.
5
+ Author-Email: Ilja Orlovs <ilja@release.art>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Programming Language :: Python :: 3.15
10
+ Classifier: Typing :: Typed
11
+ Project-URL: Homepage, https://github.com/release-art/texpkg
12
+ Project-URL: Repository, https://github.com/release-art/texpkg
13
+ Requires-Python: <3.16,>=3.15
14
+ Requires-Dist: texmodel>=0.4.0
15
+ Requires-Dist: pydantic>=2.14.1
16
+ Provides-Extra: build
17
+ Requires-Dist: typer>=0.27.3; extra == "build"
18
+ Requires-Dist: pdm-backend>=2.4; extra == "build"
19
+ Description-Content-Type: text/markdown
20
+
21
+ # texpkg
22
+
23
+ LaTeX package management.
24
+
25
+ ```zsh
26
+ pdm install
27
+ git config core.hooksPath .githooks # pre-commit runs ruff and pyright
28
+ bin/autoformat.sh
29
+ bin/test.sh # pytest, 100% coverage gate
30
+ ```
@@ -0,0 +1,41 @@
1
+ texpkg-0.1.0.dist-info/METADATA,sha256=acTlQL8FfOd65nsz51ps3DQfndOYpkKIIMrXuJ97HuM,894
2
+ texpkg-0.1.0.dist-info/WHEEL,sha256=IuIk3wbaEnFH1cx75iM2R1CmliRI3dFLXTbRakgvgFQ,90
3
+ texpkg-0.1.0.dist-info/entry_points.txt,sha256=oUqhd04qxoHQRBgbkHgEIQj5UnlKkvyrTbsBMpS-s-8,70
4
+ texpkg-0.1.0.dist-info/licenses/LICENSE,sha256=URgqTzVBt4gXkRH2QdXRAmSR42oS1Vziq6C60LxzAoc,1068
5
+ texpkg/__init__.py,sha256=ERXQOzGTJDCMgjbB-2arlapJhxkN5xv7A7HCk2SsNZM,434
6
+ texpkg/_internal/__init__.py,sha256=wuskjiFnpPCgptbkhCFXj0_z2Dl6TWm8mhNLbuFoWMw,60
7
+ texpkg/_internal/adopt.py,sha256=En46yS0EDSW8Br4pgA_7g-o5ATeSp8x7qcEX_HQDVrM,5378
8
+ texpkg/_internal/classify.py,sha256=Uxs-2D4C0KiKGuYofEEs1QltopSi80TZcAqqBQwrCQU,9147
9
+ texpkg/_internal/directory.py,sha256=ewOgbesT2Dt6Ery_AvKj8gfDS2EP3xPYnJuFgpfQT_0,2015
10
+ texpkg/_internal/envelope.py,sha256=SLj0leS-RGuDgl7KmD7xpTfvT6xf6Ux25sqRpwzucBQ,1243
11
+ texpkg/_internal/export.py,sha256=chzZYnM9Pn1eUemaN883AGAczvd5YU8c5yLXFjlJ3sw,2313
12
+ texpkg/_internal/handedits.py,sha256=YALsA0bdUhjE56_zI3Jh2YZy4owoKtHVvfAEIn8tJwo,1413
13
+ texpkg/_internal/history.py,sha256=gsxIOcC83wcV-Wea11EAz02rFcQU6ITSt1E7or-a8xs,3829
14
+ texpkg/_internal/payload.py,sha256=rjjp-uT62ElSTyvmAJ_uvIw6SaE7Vaf-_qknaIJu5sY,5114
15
+ texpkg/_internal/provenance_tex.py,sha256=bWidYrA7XrVL8ozYqz08p1rPfuhlmpWUkxHjAghpAx8,3007
16
+ texpkg/_internal/release.py,sha256=NFDnXXG922HEqa4pSlvI3dIR5gwDbm1inIhdlTaGevY,11960
17
+ texpkg/_internal/snapshots.py,sha256=p0LYdyRcDm-mwhqqsGRczk36UKajwoowzPpGvq6EX0A,2580
18
+ texpkg/_internal/state.py,sha256=37vchgRcVF2KyLx1ds4btiF692sGdSroTTSKRp_W5_Y,4942
19
+ texpkg/_internal/versions.py,sha256=r3F6B76IdtQpGOUFm56i-lJJA2nIXEAsIA93tWpC34Y,2970
20
+ texpkg/assets.py,sha256=Qsv1P5cuBeT7CWdDfNCR9qv99pLIO-6MxZBh1ckqNFg,2105
21
+ texpkg/contracts.py,sha256=3owxcK7FP2G7AZxx-ZozpeaEAnnWNzJrfhng4sG6D9g,1578
22
+ texpkg/declaration.py,sha256=M0ihNSPhNqnYQDArEyFdrzy8PXFOhE8sp5HJZTFRxWU,17475
23
+ texpkg/document.py,sha256=VK5_5R7o5OLLyOQb3f6undOOXtDVQNU9Ihpk7t7y45s,9718
24
+ texpkg/errors.py,sha256=MZWkM8MeNo8-hPlOO4yeagPhpafWMQkTnI4lkO_l-hg,2523
25
+ texpkg/layout.py,sha256=-7nymRjX9kUvcbWSRdwn1-83zkHcOGHnvMy_j5RTZic,1822
26
+ texpkg/migration.py,sha256=m3OY6DYFtY9smB5G03iZxZ9uPCGznCSDjIHrNRm6s84,2406
27
+ texpkg/protocol.py,sha256=1zJiMPvTjxk9C_PhgdYrUYueX4PV1OFWUOl5LIb9Bfw,2124
28
+ texpkg/provenance.py,sha256=9uAIcSQA124HuihuD0iuMJX-aGfvcxctZQoq-gGvCK4,2386
29
+ texpkg/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
+ texpkg_build/__init__.py,sha256=XUHwnmcgvNpiN9bcsaTgnxieq0SJ-APAReLLU_1KYpA,69
31
+ texpkg_build/_internal/__init__.py,sha256=EOIj7Sj8W9wJiKcplrtfUeyM-olcY6ctjvZzXwQlwbg,57
32
+ texpkg_build/_internal/checks.py,sha256=1fHGHJZiRgYTrp-h-6gn9QaQpt7AZvZoMl9gyPGvDaU,7000
33
+ texpkg_build/_internal/embed.py,sha256=AFholRjHQQDX4ivERetaR3Y1uFWFxBXPyCwdqy8ZbbI,12161
34
+ texpkg_build/_internal/generate.py,sha256=Vm87ZITxCV3gjIvKYKgeOnVwtHIUCOU-n-GRyWXtyoQ,10000
35
+ texpkg_build/_internal/isolate.py,sha256=BLxYvUbZ3UzACCEkPDo6oilxV-mbUug158ri7zajNK8,3659
36
+ texpkg_build/_internal/snapshot.py,sha256=sy0e6e48jT5HV3onjvZcCs-vehY0cCokznqHgPlQAsk,14085
37
+ texpkg_build/cli.py,sha256=C_vW1kZTrpQWSY-ihUYdW5GXFICrQVKczW1tGPbUL-Q,3917
38
+ texpkg_build/hook.py,sha256=M4Pj1Jsln4WyugyOiJ0miWqXXgR0qPn0FXZCOD0_Tyw,3415
39
+ texpkg_build/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
+ texpkg_build/testing.py,sha256=R4yZdB31nrNMo7jFDSPr34hdZQupt6ay_62WsgazNtY,8563
41
+ texpkg-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: pdm-backend (2.5.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,5 @@
1
+ [console_scripts]
2
+ texpkg-build = texpkg_build.cli:app
3
+
4
+ [gui_scripts]
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ilja Orlovs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ """The build of a texpkg package (never embedded in a wheel, B8)."""
@@ -0,0 +1 @@
1
+ """The internal modules of the build: not public API."""